diff --git a/.changeset/refactor-contributions-structure.md b/.changeset/refactor-contributions-structure.md new file mode 100644 index 00000000..15de42b1 --- /dev/null +++ b/.changeset/refactor-contributions-structure.md @@ -0,0 +1,26 @@ +--- +"@hypercerts-org/lexicon": minor +--- + +Refactor contributions structure and split contributor lexicon + +**Breaking Changes:** + +- **Activity lexicon (`org.hypercerts.claim.activity`):** + - Renamed `contributions` field to `contributors` + - Replaced `contributions` array (array of strongRefs) with new `contributors` array containing contributor objects + - Each contributor object has three fields: + - `contributorInformation` (required): string (DID/identifier) or strongRef to `org.hypercerts.claim.contributorInformation#main` + - `weight` (optional): positive number (stored as string) + - `contributionDetails` (optional): string or strongRef to `org.hypercerts.claim.contributionDetails#main` + - Renamed internal `contribution` object type to `contributor` + - Renamed string wrapper defs: `contributorInformationString` → `contributorIdentity`, `contributionDetailsString` → `contributorRole` + - Updated `contributorRole` string limits: maxLength 10000, maxGraphemes 1000 + +- **Contributor lexicon (`org.hypercerts.claim.contributor`):** + - Split into two separate lexicon files: + - `org.hypercerts.claim.contributorInformation`: new lexicon file containing `identifier`, `displayName`, `image` (contributor profile information) + - `org.hypercerts.claim.contributionDetails`: new lexicon file containing `role`, `contributionDescription`, `startDate`, `endDate` (contribution-specific details) + - The original `org.hypercerts.claim.contributor` lexicon has been removed + +Existing contributions using the old structure will need to be migrated to the new format. diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 58c40374..8b3e5d44 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -8,8 +8,9 @@ reviews: Pass/fail criteria: 1. If any file in lexicons/**/*.json is added, modified, or deleted: - - README.md must be updated to reflect the lexicon changes (new lexicons documented, modified properties updated, removed lexicons removed from docs) + - If README.md already references that lexicon, it must be updated to reflect the lexicon changes (modified properties updated, removed lexicons removed from docs). If it doesn't already then it's *recommended* but *not mandatory* to add some docs to it for the lexicon in question. However the README.md shouldn't duplicate what's in SCHEMAS.md. - ERD.puml must be updated if entity relationships changed (new entities added, relationships modified, entities removed) + - SCHEMAS.md must be updated with any relevant changes (e.g. fields or lexicons added/changed/removed) 2. If README.md is modified: - All documented lexicons must exist as JSON files in lexicons/ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3ae6703b..29be4f9b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,5 +26,8 @@ jobs: run: | sed -i 's|// skip|throw e;|g' node_modules/@atproto/lex-cli/dist/util.js + - name: Generate SCHEMAS.md + run: npm run gen-schemas-md + - name: Run checks run: npm run check diff --git a/.husky/pre-commit b/.husky/pre-commit index 0ad376d9..cf40d29e 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,3 @@ +npm run gen-schemas-md +npm run format npm run check diff --git a/AGENTS.md b/AGENTS.md index 53cfdd7b..e495cf1c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -238,10 +238,11 @@ dist/ # Built output (gitignored) - Auto-generate `generated/exports.ts` with all exports 3. Update `ERD.puml` as appropriate 4. Update `README.md` as appropriate -5. Run `npm run format` to ensure everything is formatted correctly +5. Run `npm run gen-schemas-md` to regenerate `SCHEMAS.md` +6. Run `npm run format` to ensure everything is formatted correctly via Prettier -6. Run `npm run check` to validate, typecheck, and build -7. **REQUIRED: Create a changeset file** in `.changeset/` directory +7. Run `npm run check` to validate, typecheck, and build +8. **REQUIRED: Create a changeset file** in `.changeset/` directory - **This step is MANDATORY for ALL changes that affect users** - See "Versioning" section below for file format - Create the file directly - do not use the interactive `npm run changeset` command diff --git a/ERD-with-fields.png b/ERD-with-fields.png index 95a86af5..b27c2c57 100644 Binary files a/ERD-with-fields.png and b/ERD-with-fields.png differ diff --git a/ERD-with-fields.svg b/ERD-with-fields.svg index e75c608e..fea1fcae 100644 --- a/ERD-with-fields.svg +++ b/ERD-with-fields.svg @@ -1 +1 @@ -Hypercert ERDHypercert ERDcontributorDID or name/pseudonymactivitytitleshortDescriptiondescription?image?workScope?startDate?endDate?contributions[]?locations[]?rights?createdAtevidencesubject?contenttitleshortDescription?description?relationType?createdAtmeasurementsubject?measurers[]metricvaluemethodType?methodURI?evidenceURI[]?location?createdAtevaluationsubject?summaryevaluators[]content[]?measurements[]?scorelocation?createdAtcontributionrole?contributors[]description?startDate?endDate?createdAtlocationlpVersionsrslocationTypelocationname?description?createdAtbadgeDefinitionbadgeTypetitleicondescription?allowedIssuers[]?createdAtbadgeAwardbadgesubjectnote?createdAtbadgeResponsebadgeAwardresponseweight?createdAtrightsrightsNamerightsTyperightsDescriptionattachment?createdAtcollectiontype?titleshortDescription?items[]createdAtdescription? (Leaflet ref)collectionProjectprojectTitle?shortProjectDescription?avatar?coverPhoto?createdAtprojectDescription (Leaflet ref)funderDID or name/pseudonymfundingReceipttofromfor?amountcurrencypaymentRail?paymentNetwork?transactionId?notes?occurredAt?createdAttoken(on-chain only)  tokenizesrecursivenestingsidecar(same TID)made byfundshasfunds \ No newline at end of file +Hypercert ERDHypercert ERDcontributor (DID/profile)DID or social profile URIactivitytitleshortDescriptiondescription?image?workScope?startDate?endDate?contributors[]?locations[]?rights?createdAtevidencesubject?contenttitleshortDescription?description?relationType?createdAtmeasurementsubject?measurers[]metricvaluemethodType?methodURI?evidenceURI[]?location?createdAtevaluationsubject?summaryevaluators[]content[]?measurements[]?scorelocation?createdAtcontributorInformationidentifier?displayName?image?createdAtcontributionDetailsrole?contributionDescription?startDate?endDate?createdAtlocationlpVersionsrslocationTypelocationname?description?createdAtbadgeDefinitionbadgeTypetitleicondescription?allowedIssuers[]?createdAtbadgeAwardbadgesubjectnote?createdAtbadgeResponsebadgeAwardresponseweight?createdAtrightsrightsNamerightsTyperightsDescriptionattachment?createdAtcollectiontype?titleshortDescription?items[]createdAtdescription? (Leaflet ref)funderDID or name/pseudonymfundingReceipttofromfor?amountcurrencypaymentRail?paymentNetwork?transactionId?notes?occurredAt?createdAttoken(on-chain only)  recursivenestingidentifiestokenizesfundshasfunds \ No newline at end of file diff --git a/ERD.png b/ERD.png index c2c2d5d4..47abab29 100644 Binary files a/ERD.png and b/ERD.png differ diff --git a/ERD.puml b/ERD.puml index b3bf1cd0..f9274498 100644 --- a/ERD.puml +++ b/ERD.puml @@ -62,7 +62,7 @@ dataclass activity <> #B4E5D0 { workScope? startDate? endDate? - contributions[]? + contributors[]? locations[]? rights? createdAt @@ -111,12 +111,19 @@ dataclass evaluation { !endif } -' org.hypercerts.claim.contributor -dataclass contributor { +' org.hypercerts.claim.contributorInformation +dataclass contributorInformation { !if (SHOW_FIELDS == "true") identifier? displayName? image? + createdAt + !endif +} + +' org.hypercerts.claim.contributionDetails +dataclass contributionDetails { + !if (SHOW_FIELDS == "true") role? contributionDescription? startDate? @@ -236,7 +243,7 @@ protocol token <> #FFB6C1 { 'evaluation --d[hidden]-> measurement 'measurement -d[hidden]-> activity 'evidence -r[hidden]-> measurement -activity -l[hidden]-> token +'activity -r[hidden]-> token 'evidence -d[hidden]-> activity 'collection -d[hidden]-> activity 'activity -u[hidden]-> contributor @@ -287,17 +294,18 @@ measurement --> location collection::items --> activity collection::items --> collection : "recursive\nnesting" -activity::contributions -l--> contributor +activity::contributors -l--> contributorInformation +activity::contributors --> contributionDetails activity::rights --> rights activity::locations --> location -contributor::identifier --> contributorEntity : made by +contributorInformation::identifier --> contributorEntity : identifies ' These ones are not described by any lexicon, but are added to ' the diagram for clarity token ..> activity : tokenizes funder ..> activity : funds -contributor ..> rights : has +contributorEntity ..> rights : has ' These two are strings which will reference the counterparty's ' DID or human-readable name (if they're not on ATProto). @@ -310,7 +318,7 @@ fundingReceipt::for --> activity : funds badgeAward::badge --> badgeDefinition badgeResponse::badgeAward --> badgeAward -badgeAward::subject --> contributor +badgeAward::subject --> contributorInformation badgeAward::subject --> activity ' This screws up the layout 'badgeAward::subject --[norank]-> collection diff --git a/ERD.svg b/ERD.svg index 42449453..eaaa1b01 100644 --- a/ERD.svg +++ b/ERD.svg @@ -1 +1 @@ -Hypercert ERDHypercert ERDcontributoractivityevidencemeasurementevaluationcontributionlocationbadgeDefinitionbadgeAwardbadgeResponserightscollectioncollectionProjectfunderfundingReceipttoken tokenizesrecursivenestingsidecar(same TID)made byfundshasfunds \ No newline at end of file +Hypercert ERDHypercert ERDcontributor (DID/profile)activityevidencemeasurementevaluationcontributorInformationcontributionDetailslocationbadgeDefinitionbadgeAwardbadgeResponserightscollectionfunderfundingReceipttoken recursivenestingidentifiestokenizesfundshasfunds \ No newline at end of file diff --git a/SCHEMAS.md b/SCHEMAS.md index af1e80cb..c6ccf0f3 100644 --- a/SCHEMAS.md +++ b/SCHEMAS.md @@ -15,22 +15,30 @@ Hypercerts-specific lexicons for tracking impact work and claims. #### Properties -| Property | Type | Required | Description | Comments | -| ------------------ | -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | -| `title` | `string` | yes | Title of the hypercert. | maxLength: 256 | -| `shortDescription` | `string` | yes | Short blurb of the impact work done. | maxLength: 3000, maxGraphemes: 300 | -| `description` | `string` | no | Optional longer description of the impact work done. | maxLength: 30000, maxGraphemes: 3000 | -| `image` | `union` | no | The hypercert visual representation as a URI or image blob. | | -| `workScope` | `ref` | no | A strong reference to a record defining the scope of work. The record referenced should describe the logical scope using label-based conditions. | | -| `startDate` | `string` | no | When the work began | | -| `endDate` | `string` | no | When the work ended | | -| `contributions` | `ref` | no | A strong reference to the contributions done to create the impact in the hypercerts. The record referenced must conform with the lexicon org.hypercerts.claim.contributor. | | -| `rights` | `ref` | no | A strong reference to the rights that this hypercert has. The record referenced must conform with the lexicon org.hypercerts.claim.rights. | | -| `locations` | `ref` | no | An array of strong references to the location where activity was performed. The record referenced must conform with the lexicon app.certified.location. | | -| `createdAt` | `string` | yes | Client-declared timestamp when this record was originally created | | +| Property | Type | Required | Description | Comments | +| ------------------ | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | +| `title` | `string` | yes | Title of the hypercert. | maxLength: 256 | +| `shortDescription` | `string` | yes | Short blurb of the impact work done. | maxLength: 3000, maxGraphemes: 300 | +| `description` | `string` | no | Optional longer description of the impact work done. | maxLength: 30000, maxGraphemes: 3000 | +| `image` | `union` | no | The hypercert visual representation as a URI or image blob. | | +| `workScope` | `ref` | no | A strong reference to a record defining the scope of work. The record referenced should describe the logical scope using label-based conditions. | | +| `startDate` | `string` | no | When the work began | | +| `endDate` | `string` | no | When the work ended | | +| `contributors` | `ref` | no | An array of contributor objects, each containing contributor information, weight, and contribution details. | | +| `rights` | `ref` | no | A strong reference to the rights that this hypercert has. The record referenced must conform with the lexicon org.hypercerts.claim.rights. | | +| `locations` | `ref` | no | An array of strong references to the location where activity was performed. The record referenced must conform with the lexicon app.certified.location. | | +| `createdAt` | `string` | yes | Client-declared timestamp when this record was originally created | | #### Defs +##### contributor + +| Property | Type | Required | Description | +| ------------------------ | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `contributorInformation` | `union` | yes | Contributor information as a string (DID or identifier) or strong reference to for instance org.hypercerts.claim.contributorInformation#main. | +| `weight` | `string` | no | The relative weight/importance of this contribution (stored as a string to avoid float precision issues). Must be a positive numeric value. Weights do not need to sum to a specific total; normalization can be performed by the consuming application as needed. | +| `contributionDetails` | `union` | no | Contribution details as a string or strong reference to org.hypercerts.claim.contributionDetails#main. | + ##### activityWeight | Property | Type | Required | Description | diff --git a/lexicons/org/hypercerts/claim/activity.json b/lexicons/org/hypercerts/claim/activity.json index 922c33a7..2b72b924 100644 --- a/lexicons/org/hypercerts/claim/activity.json +++ b/lexicons/org/hypercerts/claim/activity.json @@ -50,12 +50,12 @@ "format": "datetime", "description": "When the work ended" }, - "contributions": { + "contributors": { "type": "array", - "description": "A strong reference to the contributions done to create the impact in the hypercerts. The record referenced must conform with the lexicon org.hypercerts.claim.contributor.", + "description": "An array of contributor objects, each containing contributor information, weight, and contribution details.", "items": { "type": "ref", - "ref": "com.atproto.repo.strongRef" + "ref": "#contributor" } }, "rights": { @@ -79,6 +79,36 @@ } } }, + "contributor": { + "type": "object", + "required": ["contributorInformation"], + "properties": { + "contributorInformation": { + "type": "union", + "refs": ["#contributorIdentity", "com.atproto.repo.strongRef"], + "description": "Contributor information as a string (DID or identifier) or strong reference to for instance org.hypercerts.claim.contributorInformation#main." + }, + "weight": { + "type": "string", + "description": "The relative weight/importance of this contribution (stored as a string to avoid float precision issues). Must be a positive numeric value. Weights do not need to sum to a specific total; normalization can be performed by the consuming application as needed." + }, + "contributionDetails": { + "type": "union", + "refs": ["#contributorRole", "com.atproto.repo.strongRef"], + "description": "Contribution details as a string or strong reference to org.hypercerts.claim.contributionDetails#main." + } + } + }, + "contributorIdentity": { + "type": "string", + "description": "Contributor information as a string (DID or identifier)." + }, + "contributorRole": { + "type": "string", + "description": "Contribution details as a string.", + "maxLength": 10000, + "maxGraphemes": 1000 + }, "activityWeight": { "type": "object", "required": ["activity", "weight"], diff --git a/lexicons/org/hypercerts/claim/contributor.json b/lexicons/org/hypercerts/claim/contributionDetails.json similarity index 56% rename from lexicons/org/hypercerts/claim/contributor.json rename to lexicons/org/hypercerts/claim/contributionDetails.json index e05ff6cc..df7693b4 100644 --- a/lexicons/org/hypercerts/claim/contributor.json +++ b/lexicons/org/hypercerts/claim/contributionDetails.json @@ -1,32 +1,15 @@ { "lexicon": 1, - "id": "org.hypercerts.claim.contributor", + "id": "org.hypercerts.claim.contributionDetails", "defs": { "main": { "type": "record", - "description": "A contribution made toward a hypercert's impact.", + "description": "Details about a specific contribution including role, description, and timeframe.", "key": "tid", "record": { "type": "object", "required": ["createdAt"], "properties": { - "identifier": { - "type": "string", - "description": "DID or a URI to a social profile of the contributor." - }, - "displayName": { - "type": "string", - "description": "Display name of the contributor.", - "maxLength": 100 - }, - "image": { - "type": "union", - "refs": [ - "org.hypercerts.defs#uri", - "org.hypercerts.defs#smallImage" - ], - "description": "The contributor visual representation as a URI or image blob." - }, "role": { "type": "string", "description": "Role or title of the contributor.", @@ -34,9 +17,9 @@ }, "contributionDescription": { "type": "string", - "description": "What the contribution concretely achieved.", - "maxLength": 2000, - "maxGraphemes": 500 + "description": "What the contribution concretely was.", + "maxLength": 10000, + "maxGraphemes": 1000 }, "startDate": { "type": "string", diff --git a/lexicons/org/hypercerts/claim/contributorInformation.json b/lexicons/org/hypercerts/claim/contributorInformation.json new file mode 100644 index 00000000..742a0657 --- /dev/null +++ b/lexicons/org/hypercerts/claim/contributorInformation.json @@ -0,0 +1,39 @@ +{ + "lexicon": 1, + "id": "org.hypercerts.claim.contributorInformation", + "defs": { + "main": { + "type": "record", + "description": "Contributor information including identifier, display name, and image.", + "key": "tid", + "record": { + "type": "object", + "required": ["createdAt"], + "properties": { + "identifier": { + "type": "string", + "description": "DID or a URI to a social profile of the contributor." + }, + "displayName": { + "type": "string", + "description": "Display name of the contributor.", + "maxLength": 100 + }, + "image": { + "type": "union", + "refs": [ + "org.hypercerts.defs#uri", + "org.hypercerts.defs#smallImage" + ], + "description": "The contributor visual representation as a URI or image blob." + }, + "createdAt": { + "type": "string", + "format": "datetime", + "description": "Client-declared timestamp when this record was originally created." + } + } + } + } + } +} diff --git a/package.json b/package.json index dad2e860..3221c0b6 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ }, "scripts": { "list": "find ./lexicons -name '*.json'", - "check": "npm run gen-schemas-md && npm run gen-api && npm run lint && npm run typecheck && npm run build && npm run test", + "check": "npm run gen-api && npm run lint && npm run typecheck && npm run build && npm run test", "build": "rollup -c && npm run build:types", "build:types": "tsc --project tsconfig.build.json", "gen-api": "find ./lexicons -name '*.json' | xargs lex gen-api --yes ./generated && ./scripts/create-shims.sh && npm run gen-index", diff --git a/scripts/generate-schemas.js b/scripts/generate-schemas.js index 51764cbc..e373aba0 100644 --- a/scripts/generate-schemas.js +++ b/scripts/generate-schemas.js @@ -273,11 +273,16 @@ function generateLexiconSection(lexicon, isFirst = false) { if (additionalDefs.length > 0) { output.push("", "#### Defs", ""); - for (const [defName, defData] of additionalDefs) { + for (let i = 0; i < additionalDefs.length; i++) { + const [defName, defData] = additionalDefs[i]; output.push(`##### ${defName}`, ""); const defRows = extractPropertyRows(defData, defData.required || []); if (defRows.length > 0) { output.push(...renderTable(defRows, false)); + // Add blank line after table only if not the last def + if (i < additionalDefs.length - 1) { + output.push(""); + } } } }