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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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("");
+ }
}
}
}