From cc9d7bff3dc63f155ce8e11204fc1506ca687711 Mon Sep 17 00:00:00 2001 From: bitbeckers Date: Tue, 20 Jan 2026 17:38:36 +1300 Subject: [PATCH 1/3] refactor(collection): replace activities with items array - Replace activities array (activityWeight) with items array (strongRef) - Items can reference activities and/or other collections (recursive nesting) - Update collection description to reflect new structure - Update README with example showing recursive nesting - Update ERD to show items field and recursive relationship --- .changeset/refactor-collection-items-array.md | 5 ++ ERD.puml | 16 +++- README.md | 83 ++++--------------- lexicons/org/hypercerts/claim/collection.json | 10 +-- 4 files changed, 41 insertions(+), 73 deletions(-) create mode 100644 .changeset/refactor-collection-items-array.md diff --git a/.changeset/refactor-collection-items-array.md b/.changeset/refactor-collection-items-array.md new file mode 100644 index 00000000..65ced531 --- /dev/null +++ b/.changeset/refactor-collection-items-array.md @@ -0,0 +1,5 @@ +--- +"@hypercerts-org/lexicon": minor +--- + +Refactor collection lexicon to use items array instead of activities. The items array contains plain strongRefs (com.atproto.repo.strongRef) that can reference activities (org.hypercerts.claim.activity) and/or other collections (org.hypercerts.claim.collection), enabling recursive collection nesting. This change removes the activityWeight object structure from the base collection lexicon. diff --git a/ERD.puml b/ERD.puml index 885616a6..96df25a8 100644 --- a/ERD.puml +++ b/ERD.puml @@ -185,6 +185,18 @@ dataclass collection { type title shortDescription? + avatar? + coverPhoto? + items[] + createdAt + !endif +} + +' org.hypercerts.claim.project +dataclass project { + !if (SHOW_FIELDS == "true") + title + shortDescription description? avatar? coverPhoto? @@ -285,8 +297,8 @@ hiddenLocation2 -[hidden]-> location measurement --> location !endif -collection::activities --> activity -collection ..|> location : "sidecar\n(same TID)" +collection::items --> activity +collection::items --> collection : "recursive\nnesting" project::activities --> activity project::location --> location diff --git a/README.md b/README.md index 3d976c7e..19fe74bf 100644 --- a/README.md +++ b/README.md @@ -405,7 +405,7 @@ Hypercerts-specific lexicons for tracking impact work and claims. **Lexicon ID:** `org.hypercerts.claim.collection` -**Description:** A collection/group of items (activities and/or other collections). Collections support recursive nesting. Use `org.hypercerts.claim.collection.project` as a sidecar (same TID) for project-specific metadata. +**Description:** A collection/group of items (activities and/or other collections). Collections support recursive nesting, allowing collections to contain other collections. Use app.certified.location as a sidecar (same TID) for location metadata. **Key:** `tid` @@ -428,6 +428,8 @@ import { TID } from "@atproto/common"; const collectionRecord = { $type: "org.hypercerts.claim.collection", collectionTitle: "Climate Action Projects", + shortDescription: + "A collection of climate-related activities and sub-collections", items: [ // Reference to an activity { @@ -451,77 +453,26 @@ const collectionRecord = { --- -### org.hypercerts.claim.collection.project +### org.hypercerts.claim.project -**Lexicon ID:** `org.hypercerts.claim.collection.project` +**Lexicon ID:** `org.hypercerts.claim.project` -**Description:** Project-specific metadata for a collection. Uses the sidecar pattern with the same record key (TID) as the collection record. This allows collections to represent projects by adding rich-text descriptions and visual assets. +**Description:** A project that can include multiple activities -**Key:** `tid` (same as the collection record) +**Key:** `tid` #### Properties -| Property | Type | Required | Description | Comments | -| ------------------------- | -------- | -------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | -| `projectTitle` | `string` | ❌ | The title of this project | maxLength: 800, maxGraphemes: 80 | -| `shortProjectDescription` | `string` | ❌ | Short summary of this project, suitable for previews and list views | maxLength: 3000, maxGraphemes: 300 | -| `projectDescription` | `ref` | ✅ | Rich-text description of this project, represented as a Leaflet linear document | References must conform to `pub.leaflet.pages.linearDocument#main` | -| `avatar` | `blob` | ❌ | Primary avatar image representing this project across apps and views | Typically a square logo or project identity image; image/png or image/jpeg, maxSize: 1000000 | -| `coverPhoto` | `blob` | ❌ | The cover photo of this project | image/png or image/jpeg, maxSize: 1000000 | -| `createdAt` | `string` | ✅ | Client-declared timestamp when this project metadata was created | | - -#### Example: Creating a Project (Collection + Project Sidecar) - -```typescript -import { TID } from "@atproto/common"; - -const tid = TID.nextStr(); // Same TID for both records - -// Base collection record -const collectionRecord = { - $type: "org.hypercerts.claim.collection", - collectionTitle: "Carbon Offset Initiative", - items: [ - { - uri: "at://did:plc:alice/org.hypercerts.claim.activity/3k2abc", - cid: "...", - }, - { - uri: "at://did:plc:bob/org.hypercerts.claim.activity/7x9def", - cid: "...", - }, - ], - createdAt: new Date().toISOString(), -}; - -// Project sidecar with rich-text description and assets -const projectSidecar = { - $type: "org.hypercerts.claim.collection.project", - projectTitle: "Carbon Offset Initiative", - shortProjectDescription: "A project focused on carbon reduction", - projectDescription: { - uri: "at://did:plc:alice/pub.leaflet.pages.linearDocument/abc123", - cid: "...", - }, - avatar: avatarBlob, - coverPhoto: coverPhotoBlob, - createdAt: new Date().toISOString(), -}; - -// Create both with same TID -await createRecord({ - collection: "org.hypercerts.claim.collection", - rkey: tid, - record: collectionRecord, -}); -await createRecord({ - collection: "org.hypercerts.claim.collection.project", - rkey: tid, - record: projectSidecar, -}); -``` - -**Note**: The project sidecar is optional. Collections without this sidecar are simple groupings; collections with it are "projects" with rich documentation. +| Property | Type | Required | Description | Comments | +| ------------------ | -------- | -------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------- | +| `title` | `string` | ✅ | Title of this project | | +| `shortDescription` | `string` | ✅ | Short summary of this project, suitable for previews and list views | | +| `description` | `ref` | ❌ | Rich-text description of this project, represented as a Leaflet linear document | References must conform to `pub.leaflet.pages.linearDocument#main` | +| `avatar` | `blob` | ❌ | Primary avatar image representing this project across apps and views | Typically a square logo or project identity image | +| `coverPhoto` | `blob` | ❌ | The cover photo of this project | | +| `activities` | `array` | ❌ | Array of activities with their associated weights in this project | Each item references `org.hypercerts.claim.activity#activityWeight` | +| `location` | `ref` | ❌ | A strong reference to a location record describing where the work took place | References must conform to `app.certified.location` | +| `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created | | --- diff --git a/lexicons/org/hypercerts/claim/collection.json b/lexicons/org/hypercerts/claim/collection.json index 7981bbf4..dd97fc34 100644 --- a/lexicons/org/hypercerts/claim/collection.json +++ b/lexicons/org/hypercerts/claim/collection.json @@ -4,11 +4,11 @@ "defs": { "main": { "type": "record", - "description": "A group of hypercerts that have a specific property. Can be a project or a collection.", + "description": "A collection/group of items (activities and/or other collections). Collections support recursive nesting. Use app.certified.location as a sidecar (same TID) for location metadata.", "key": "tid", "record": { "type": "object", - "required": ["type", "title", "createdAt"], + "required": ["title", "items", "createdAt"], "properties": { "type": { "type": "string", @@ -43,12 +43,12 @@ "accept": ["image/png", "image/jpeg"], "maxSize": 1000000 }, - "activities": { + "items": { "type": "array", - "description": "Array of activities with their associated weights in this collection", + "description": "Array of strong references to items in this collection. Items can be activities (org.hypercerts.claim.activity) and/or other collections (org.hypercerts.claim.collection).", "items": { "type": "ref", - "ref": "org.hypercerts.claim.activity#activityWeight" + "ref": "com.atproto.repo.strongRef" } }, "createdAt": { From c55d8a77ff2949136bab0c6680b7e458712404f1 Mon Sep 17 00:00:00 2001 From: bitbeckers Date: Tue, 20 Jan 2026 17:46:10 +1300 Subject: [PATCH 2/3] feat(collection): add project sidecar and remove project lexicon - Delete org.hypercerts.claim.project lexicon - Create org.hypercerts.claim.collection.project sidecar - Move avatar and coverPhoto from collection to project sidecar - Projects are now collections + project sidecar (same TID) - Update README with project sidecar documentation and example - Update ERD to show collectionProject sidecar entity --- .changeset/add-project-sidecar.md | 5 ++ ERD.puml | 23 ++++-- README.md | 80 +++++++++++++++---- lexicons/org/hypercerts/claim/collection.json | 33 +++----- .../hypercerts/claim/collection/project.json | 51 ++++++++++++ 5 files changed, 148 insertions(+), 44 deletions(-) create mode 100644 .changeset/add-project-sidecar.md create mode 100644 lexicons/org/hypercerts/claim/collection/project.json diff --git a/.changeset/add-project-sidecar.md b/.changeset/add-project-sidecar.md new file mode 100644 index 00000000..e3f239a9 --- /dev/null +++ b/.changeset/add-project-sidecar.md @@ -0,0 +1,5 @@ +--- +"@hypercerts-org/lexicon": minor +--- + +Remove org.hypercerts.claim.project lexicon and replace with org.hypercerts.claim.collection.project sidecar. Projects are now represented as collections with an optional project sidecar (same TID) that provides rich-text descriptions, avatars, and cover photos. Avatar and coverPhoto fields moved from base collection to project sidecar. Collections without the project sidecar are simple groupings; collections with it are "projects" with rich documentation. diff --git a/ERD.puml b/ERD.puml index 96df25a8..555fe630 100644 --- a/ERD.puml +++ b/ERD.puml @@ -182,17 +182,28 @@ dataclass rights { ' by creating a location record with the same TID dataclass collection { !if (SHOW_FIELDS == "true") - type - title - shortDescription? + collectionTitle + shortCollectionDescription? + collectionDescription? + items[] + location? + createdAt + !endif +} + +' org.hypercerts.claim.collection.project (sidecar) +dataclass collectionProject { + !if (SHOW_FIELDS == "true") + projectTitle? + shortProjectDescription? + projectDescription (Leaflet ref) avatar? coverPhoto? - items[] createdAt !endif } -' org.hypercerts.claim.project +' org.hypercerts.claim.project (DEPRECATED) dataclass project { !if (SHOW_FIELDS == "true") title @@ -299,6 +310,8 @@ measurement --> location collection::items --> activity collection::items --> collection : "recursive\nnesting" +collection::location --> location +collection ..|> collectionProject : "sidecar\n(same TID)" project::activities --> activity project::location --> location diff --git a/README.md b/README.md index 19fe74bf..c03b0a5b 100644 --- a/README.md +++ b/README.md @@ -417,7 +417,6 @@ Hypercerts-specific lexicons for tracking impact work and claims. | `shortCollectionDescription` | `string` | ❌ | Short summary of this collection, suitable for previews and list views | | | `collectionDescription` | `string` | ❌ | Full description of this collection, suitable for detail views | | | `items` | `array` | ✅ | Array of strong references to items in this collection | Items can be activities (`org.hypercerts.claim.activity`) and/or other collections (`org.hypercerts.claim.collection`). Enables recursive nesting. | -| `location` | `ref` | ❌ | A strong reference to a location record describing where the work took place | The referenced record must conform to the `app.certified.location` lexicon. | | `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created | | #### Example: Creating a Collection with Nested Items @@ -453,26 +452,77 @@ const collectionRecord = { --- -### org.hypercerts.claim.project +### org.hypercerts.claim.collection.project -**Lexicon ID:** `org.hypercerts.claim.project` +**Lexicon ID:** `org.hypercerts.claim.collection.project` -**Description:** A project that can include multiple activities +**Description:** Project-specific metadata for a collection. Uses the sidecar pattern with the same record key (TID) as the collection record. This allows collections to represent projects by adding rich-text descriptions and visual assets. -**Key:** `tid` +**Key:** `tid` (same as the collection record) #### Properties -| Property | Type | Required | Description | Comments | -| ------------------ | -------- | -------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------- | -| `title` | `string` | ✅ | Title of this project | | -| `shortDescription` | `string` | ✅ | Short summary of this project, suitable for previews and list views | | -| `description` | `ref` | ❌ | Rich-text description of this project, represented as a Leaflet linear document | References must conform to `pub.leaflet.pages.linearDocument#main` | -| `avatar` | `blob` | ❌ | Primary avatar image representing this project across apps and views | Typically a square logo or project identity image | -| `coverPhoto` | `blob` | ❌ | The cover photo of this project | | -| `activities` | `array` | ❌ | Array of activities with their associated weights in this project | Each item references `org.hypercerts.claim.activity#activityWeight` | -| `location` | `ref` | ❌ | A strong reference to a location record describing where the work took place | References must conform to `app.certified.location` | -| `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created | | +| Property | Type | Required | Description | Comments | +| ------------------------- | -------- | -------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | +| `projectTitle` | `string` | ❌ | The title of this collection | maxLength: 800, maxGraphemes: 80 | +| `shortProjectDescription` | `string` | ❌ | Short summary of this project, suitable for previews and list views | maxLength: 3000, maxGraphemes: 300 | +| `projectDescription` | `ref` | ✅ | Rich-text description of this project, represented as a Leaflet linear document | References must conform to `pub.leaflet.pages.linearDocument#main` | +| `avatar` | `blob` | ❌ | Primary avatar image representing this project across apps and views | Typically a square logo or project identity image; image/png or image/jpeg, maxSize: 1000000 | +| `coverPhoto` | `blob` | ❌ | The cover photo of this project | image/png or image/jpeg, maxSize: 1000000 | +| `createdAt` | `string` | ✅ | Client-declared timestamp when this project metadata was created | | + +#### Example: Creating a Project (Collection + Project Sidecar) + +```typescript +import { TID } from "@atproto/common"; + +const tid = TID.nextStr(); // Same TID for both records + +// Base collection record +const collectionRecord = { + $type: "org.hypercerts.claim.collection", + title: "Carbon Offset Initiative", + items: [ + { + uri: "at://did:plc:alice/org.hypercerts.claim.activity/3k2abc", + cid: "...", + }, + { + uri: "at://did:plc:bob/org.hypercerts.claim.activity/7x9def", + cid: "...", + }, + ], + createdAt: new Date().toISOString(), +}; + +// Project sidecar with rich-text description and assets +const projectSidecar = { + $type: "org.hypercerts.claim.collection.project", + projectTitle: "Carbon Offset Initiative", + shortProjectDescription: "A project focused on carbon reduction", + projectDescription: { + uri: "at://did:plc:alice/pub.leaflet.pages.linearDocument/abc123", + cid: "...", + }, + avatar: avatarBlob, + coverPhoto: coverPhotoBlob, + createdAt: new Date().toISOString(), +}; + +// Create both with same TID +await createRecord({ + collection: "org.hypercerts.claim.collection", + rkey: tid, + record: collectionRecord, +}); +await createRecord({ + collection: "org.hypercerts.claim.collection.project", + rkey: tid, + record: projectSidecar, +}); +``` + +**Note**: The project sidecar is optional. Collections without this sidecar are simple groupings; collections with it are "projects" with rich documentation. --- diff --git a/lexicons/org/hypercerts/claim/collection.json b/lexicons/org/hypercerts/claim/collection.json index dd97fc34..66302da0 100644 --- a/lexicons/org/hypercerts/claim/collection.json +++ b/lexicons/org/hypercerts/claim/collection.json @@ -8,40 +8,25 @@ "key": "tid", "record": { "type": "object", - "required": ["title", "items", "createdAt"], + "required": ["collectionTitle", "items", "createdAt"], "properties": { - "type": { - "type": "string", - "description": "The type of this group. Can be 'project', 'collection', or any other custom type string." - }, - "title": { + "collectionTitle": { "type": "string", "description": "The title of this collection", "maxLength": 800, "maxGraphemes": 80 }, - "shortDescription": { + "shortCollectionDescription": { "type": "string", "maxLength": 3000, "maxGraphemes": 300, - "description": "A short description of this collection" - }, - "description": { - "type": "ref", - "ref": "pub.leaflet.pages.linearDocument#main", - "description": "Rich-text description, represented as a Leaflet linear document." - }, - "avatar": { - "type": "blob", - "description": "Primary avatar image representing this collection across apps and views; typically a square image.", - "accept": ["image/png", "image/jpeg"], - "maxSize": 1000000 + "description": "Short summary of this collection, suitable for previews and list views" }, - "coverPhoto": { - "type": "blob", - "description": "The cover photo of this collection.", - "accept": ["image/png", "image/jpeg"], - "maxSize": 1000000 + "collectionDescription": { + "type": "string", + "maxLength": 3000, + "maxGraphemes": 300, + "description": "Full description of this collection, suitable for detail views" }, "items": { "type": "array", diff --git a/lexicons/org/hypercerts/claim/collection/project.json b/lexicons/org/hypercerts/claim/collection/project.json new file mode 100644 index 00000000..a1385694 --- /dev/null +++ b/lexicons/org/hypercerts/claim/collection/project.json @@ -0,0 +1,51 @@ +{ + "lexicon": 1, + "id": "org.hypercerts.claim.collection.project", + "defs": { + "main": { + "type": "record", + "description": "Project-specific metadata for a collection. Uses the sidecar pattern with the same record key (TID) as the collection record. Provides rich-text description capabilities for project-type collections.", + "key": "tid", + "record": { + "type": "object", + "required": ["projectDescription", "createdAt"], + "properties": { + "projectTitle": { + "type": "string", + "description": "The title of this collection", + "maxLength": 800, + "maxGraphemes": 80 + }, + "shortProjectDescription": { + "type": "string", + "maxLength": 3000, + "maxGraphemes": 300, + "description": "Short summary of this project, suitable for previews and list views" + }, + "projectDescription": { + "type": "ref", + "ref": "pub.leaflet.pages.linearDocument#main", + "description": "Rich-text description of this project, represented as a Leaflet linear document." + }, + "avatar": { + "type": "blob", + "description": "Primary avatar image representing this project across apps and views; typically a square logo or project identity image.", + "accept": ["image/png", "image/jpeg"], + "maxSize": 1000000 + }, + "coverPhoto": { + "type": "blob", + "description": "The cover photo of this project.", + "accept": ["image/png", "image/jpeg"], + "maxSize": 1000000 + }, + "createdAt": { + "type": "string", + "format": "datetime", + "description": "Client-declared timestamp when this project metadata was created" + } + } + } + } + } +} From f1c6f284a291c1d710c16835811f92e958a26d73 Mon Sep 17 00:00:00 2001 From: bitbeckers Date: Tue, 20 Jan 2026 20:02:17 +1300 Subject: [PATCH 3/3] chore(doc): update documentation --- ERD.puml | 19 ------------------- README.md | 18 +++++++++--------- 2 files changed, 9 insertions(+), 28 deletions(-) diff --git a/ERD.puml b/ERD.puml index 555fe630..a5bdbc10 100644 --- a/ERD.puml +++ b/ERD.puml @@ -186,7 +186,6 @@ dataclass collection { shortCollectionDescription? collectionDescription? items[] - location? createdAt !endif } @@ -203,20 +202,6 @@ dataclass collectionProject { !endif } -' org.hypercerts.claim.project (DEPRECATED) -dataclass project { - !if (SHOW_FIELDS == "true") - title - shortDescription - description? - avatar? - coverPhoto? - activities[] - location? - createdAt - !endif -} - 'together { ' Funders are represented by DIDs or human-readable strings @@ -310,12 +295,8 @@ measurement --> location collection::items --> activity collection::items --> collection : "recursive\nnesting" -collection::location --> location collection ..|> collectionProject : "sidecar\n(same TID)" -project::activities --> activity -project::location --> location - activity::contributions -l--> contribution activity::rights --> rights activity::locations --> location diff --git a/README.md b/README.md index c03b0a5b..b8011877 100644 --- a/README.md +++ b/README.md @@ -411,13 +411,13 @@ Hypercerts-specific lexicons for tracking impact work and claims. #### Properties -| Property | Type | Required | Description | Comments | -| ---------------------------- | -------- | -------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | -| `collectionTitle` | `string` | ✅ | The title of this collection | | -| `shortCollectionDescription` | `string` | ❌ | Short summary of this collection, suitable for previews and list views | | -| `collectionDescription` | `string` | ❌ | Full description of this collection, suitable for detail views | | -| `items` | `array` | ✅ | Array of strong references to items in this collection | Items can be activities (`org.hypercerts.claim.activity`) and/or other collections (`org.hypercerts.claim.collection`). Enables recursive nesting. | -| `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created | | +| Property | Type | Required | Description | Comments | +| ---------------------------- | -------- | -------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| `collectionTitle` | `string` | ✅ | The title of this collection | maxLength: 800, maxGraphemes: 80 | +| `shortCollectionDescription` | `string` | ❌ | Short summary of this collection, suitable for previews and list views | maxLength: 3000, maxGraphemes: 300 | +| `collectionDescription` | `string` | ❌ | Full description of this collection, suitable for detail views | maxLength: 3000, maxGraphemes: 300 | +| `items` | `array` | ✅ | Array of strong references to items in this collection | Items can be activities (`org.hypercerts.claim.activity`) and/or other collections (`org.hypercerts.claim.collection`). Enables recursive nesting. | +| `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created | | #### Example: Creating a Collection with Nested Items @@ -427,7 +427,7 @@ import { TID } from "@atproto/common"; const collectionRecord = { $type: "org.hypercerts.claim.collection", collectionTitle: "Climate Action Projects", - shortDescription: + shortCollectionDescription: "A collection of climate-related activities and sub-collections", items: [ // Reference to an activity @@ -481,7 +481,7 @@ const tid = TID.nextStr(); // Same TID for both records // Base collection record const collectionRecord = { $type: "org.hypercerts.claim.collection", - title: "Carbon Offset Initiative", + collectionTitle: "Carbon Offset Initiative", items: [ { uri: "at://did:plc:alice/org.hypercerts.claim.activity/3k2abc",