Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/add-project-sidecar.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions .changeset/refactor-collection-items-array.md
Original file line number Diff line number Diff line change
@@ -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.
28 changes: 17 additions & 11 deletions ERD.puml
Original file line number Diff line number Diff line change
Expand Up @@ -182,14 +182,22 @@ dataclass rights {
' by creating a location record with the same TID
dataclass collection {
!if (SHOW_FIELDS == "true")
type
title
shortDescription?
description?
collectionTitle
shortCollectionDescription?
collectionDescription?
items[]
createdAt
!endif
}

' org.hypercerts.claim.collection.project (sidecar)
dataclass collectionProject {
!if (SHOW_FIELDS == "true")
projectTitle?
shortProjectDescription?
projectDescription (Leaflet ref)
avatar?
coverPhoto?
activities[]
location?
createdAt
!endif
}
Expand Down Expand Up @@ -285,11 +293,9 @@ hiddenLocation2 -[hidden]-> location
measurement --> location
!endif

collection::activities --> activity
collection ..|> location : "sidecar\n(same TID)"

project::activities --> activity
project::location --> location
collection::items --> activity
collection::items --> collection : "recursive\nnesting"
collection ..|> collectionProject : "sidecar\n(same TID)"

activity::contributions -l--> contribution
activity::rights --> rights
Expand Down
21 changes: 11 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -405,20 +405,19 @@ 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`

#### 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. |
| `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 | |
| 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

Expand All @@ -428,6 +427,8 @@ import { TID } from "@atproto/common";
const collectionRecord = {
$type: "org.hypercerts.claim.collection",
collectionTitle: "Climate Action Projects",
shortCollectionDescription:
"A collection of climate-related activities and sub-collections",
items: [
// Reference to an activity
{
Expand Down Expand Up @@ -463,7 +464,7 @@ const collectionRecord = {

| Property | Type | Required | Description | Comments |
| ------------------------- | -------- | -------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `projectTitle` | `string` | ❌ | The title of this project | maxLength: 800, maxGraphemes: 80 |
| `projectTitle` | `string` | ❌ | The title of this collection | maxLength: 800, maxGraphemes: 80 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor: Correct the projectTitle description.

The description says "The title of this collection" but should say "The title of this project" to match the sidecar context.

📝 Suggested fix
-| `projectTitle`            | `string` | ❌       | The title of this collection                                                    | maxLength: 800, maxGraphemes: 80                                             |
+| `projectTitle`            | `string` | ❌       | The title of this project                                                       | maxLength: 800, maxGraphemes: 80                                             |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| `projectTitle` | `string` || The title of this collection | maxLength: 800, maxGraphemes: 80 |
| `projectTitle` | `string` || The title of this project | maxLength: 800, maxGraphemes: 80 |
🤖 Prompt for AI Agents
In `@README.md` at line 467, Update the README entry for the `projectTitle` field
so its description reads "The title of this project" instead of "The title of
this collection"; locate the table row containing the `projectTitle` symbol and
change only the descriptive text to match the sidecar context while keeping the
rest of the constraints (maxLength: 800, maxGraphemes: 80) unchanged.

| `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 |
Expand Down
41 changes: 13 additions & 28 deletions lexicons/org/hypercerts/claim/collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,51 +4,36 @@
"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": ["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"
},
"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": {
Expand Down
51 changes: 51 additions & 0 deletions lexicons/org/hypercerts/claim/collection/project.json
Original file line number Diff line number Diff line change
@@ -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."
},
Comment on lines +25 to +29

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's see the full project.json file to understand the structure
cat -n lexicons/org/hypercerts/claim/collection/project.json

Repository: hypercerts-org/hypercerts-lexicon

Length of output: 2332


🏁 Script executed:

# Search for strongRef usage in other lexicon files to understand the pattern
rg "strongRef" lexicons/ -A 2 -B 2

Repository: hypercerts-org/hypercerts-lexicon

Length of output: 6317


🏁 Script executed:

# Check for usage of pub.leaflet.pages.linearDocument in the codebase
rg "linearDocument" --include="*.json" --include="*.md"

Repository: hypercerts-org/hypercerts-lexicon

Length of output: 518


🏁 Script executed:

# Check README.md for documentation about projectDescription
grep -n "projectDescription\|project.json\|collection" README.md | head -20

Repository: hypercerts-org/hypercerts-lexicon

Length of output: 2246


🏁 Script executed:

# Look for other examples of 'ref' type fields in the codebase
rg '"type":\s*"ref"' lexicons/ -A 1 -B 1 | head -40

Repository: hypercerts-org/hypercerts-lexicon

Length of output: 2482


🏁 Script executed:

# Search for Leaflet lexicon files
find lexicons -name "*leaflet*" -o -name "*linear*"

Repository: hypercerts-org/hypercerts-lexicon

Length of output: 59


🏁 Script executed:

# Look for pub.bsky patterns to understand ATProto definition refs
rg "pub\.bsky" lexicons/ | head -20

Repository: hypercerts-org/hypercerts-lexicon

Length of output: 59


🏁 Script executed:

# Check if there are any inline type definitions vs record references
rg '"type":\s*"ref"' lexicons/ -B 2 | grep -E "(ref|description)" | head -30

Repository: hypercerts-org/hypercerts-lexicon

Length of output: 2968


🏁 Script executed:

# Search for documentation on how refs are used in ATProto
rg "richtext\|facet" lexicons/ -l

Repository: hypercerts-org/hypercerts-lexicon

Length of output: 59


🌐 Web query:

ATProto lexicon how to reference external lexicon definitions in ref fields

💡 Result:

Use a full NSID (optionally with a fragment) as the ref string. Per the Lexicon spec, a ref value may be:

  • a global reference: the Lexicon NSID, optionally with a “#definitionName” fragment (e.g. com.example.defs#MyType), or
  • a local reference: a fragment-only string that points to a definition in the same file (e.g. #MyType). [1]

Notes/rules:

  • If the referenced definition is the file’s main definition, you can reference it by NSID without a fragment (e.g. com.example.record). Do not use a #main suffix in $type fields. [1]
  • Refs may not point to token types. [1]

Example ref entries:

  • External object type: { "type": "ref", "ref": "com.acme.defs#Address" }
  • Local definition: { "type": "ref", "ref": "#Address" }
  • Main definition reference (used in $type): "com.acme.person" (not "com.acme.person#main"). [1]

Source: AT Protocol Lexicon specification. [1]


🏁 Script executed:

# Check if pub.leaflet exists anywhere in the repo or if it's external
find . -type f -name "*.json" | xargs grep -l "leaflet\|pub\.leaflet" 2>/dev/null | head -10

Repository: hypercerts-org/hypercerts-lexicon

Length of output: 183


🏁 Script executed:

# Look at existing strongRef definition to understand the pattern
cat -n lexicons/com/atproto/repo/strongRef.json

Repository: hypercerts-org/hypercerts-lexicon

Length of output: 532


🏁 Script executed:

# Check how the README currently describes projectDescription to compare with review claim
sed -n '450,475p' README.md

Repository: hypercerts-org/hypercerts-lexicon

Length of output: 2492


🏁 Script executed:

# Check if there are any examples in the README showing how projectDescription should be used
sed -n '475,530p' README.md

Repository: hypercerts-org/hypercerts-lexicon

Length of output: 1661


🏁 Script executed:

# Search for any other direct definition references (not strongRef) to see if this is a pattern
rg '"ref":\s*"[^c]' lexicons/ | grep -v strongRef | grep -v "did" | head -20

Repository: hypercerts-org/hypercerts-lexicon

Length of output: 525


🏁 Script executed:

# Check the full context around where projectDescription is used in examples
sed -n '475,510p' README.md

Repository: hypercerts-org/hypercerts-lexicon

Length of output: 1082


Fix projectDescription ref to match the strongRef pattern shown in README examples.

The schema uses "ref": "pub.leaflet.pages.linearDocument#main", but the README example shows projectDescription receiving a strongRef-shaped value ({ uri, cid }). This mismatch will cause validation failures when clients submit valid payloads matching the documented example. All other cross-record references in the codebase use com.atproto.repo.strongRef; this field should too.

Proposed fix
           "projectDescription": {
             "type": "ref",
-            "ref": "pub.leaflet.pages.linearDocument#main",
-            "description": "Rich-text description of this project, represented as a Leaflet linear document."
+            "ref": "com.atproto.repo.strongRef",
+            "description": "Strong reference to a Leaflet linear document describing this project. The referenced record must conform to pub.leaflet.pages.linearDocument#main."
           },
🤖 Prompt for AI Agents
In `@lexicons/org/hypercerts/claim/collection/project.json` around lines 25 - 29,
The projectDescription field currently points to
"pub.leaflet.pages.linearDocument#main" but should use the strongRef pattern so
clients submit the { uri, cid } shape shown in the README; update the
projectDescription schema to reference com.atproto.repo.strongRef (replace the
existing "ref": "pub.leaflet.pages.linearDocument#main") and keep or adjust the
description to indicate it stores a strongRef to the Leaflet linear document so
validation matches other cross-record refs (e.g., com.atproto.repo.strongRef)
used in the codebase.

"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"
}
}
}
}
}
}