diff --git a/.changeset/add-cel-work-scope-expressions.md b/.changeset/add-cel-work-scope-expressions.md new file mode 100644 index 00000000..c26b0a17 --- /dev/null +++ b/.changeset/add-cel-work-scope-expressions.md @@ -0,0 +1,5 @@ +--- +"@hypercerts-org/lexicon": minor +--- + +Add CEL expression support for structured work scopes (`org.hypercerts.workscope.cel`, `org.hypercerts.workscope.tag`) diff --git a/ERD.puml b/ERD.puml index 3d03e784..ca818630 100644 --- a/ERD.puml +++ b/ERD.puml @@ -191,6 +191,33 @@ dataclass badgeResponse { !endif } +' org.hypercerts.workscope.cel +dataclass celExpression { + !if (SHOW_FIELDS == "true") + expression + usedTags[] + version + createdAt + !endif +} + +' org.hypercerts.workscope.tag +dataclass workScopeTag { + !if (SHOW_FIELDS == "true") + key + name + category? + description? + parent? + status? + supersededBy? + aliases[]? + sameAs[]? + referenceDocument? + createdAt + !endif +} + ' org.hypercerts.claim.rights dataclass rights { !if (SHOW_FIELDS == "true") @@ -327,11 +354,17 @@ acknowledgement::subject --> contributorInformation acknowledgement::context --> collection acknowledgement::context --> activity +activity::workScope --> celExpression activity::contributors -l--> contributorInformation activity::contributors --> contribution activity::rights --> rights activity::locations --> location +celExpression::usedTags --> workScopeTag : "strongRef" + +workScopeTag::parent --> workScopeTag : "hierarchy" +workScopeTag::supersededBy --> workScopeTag : "deprecation" + contributorInformation::identifier --> contributorEntity : identifies organization::location --> location diff --git a/README.md b/README.md index ac35336a..2dcb78a1 100644 --- a/README.md +++ b/README.md @@ -65,12 +65,27 @@ const activityRecord = { $type: ACTIVITY_NSID, title: "My Impact Work", shortDescription: "Description here", - // workScope can be a strongRef to a work-scope record: + // workScope can be a CEL expression (structured, machine-evaluable): workScope: { - uri: "at://did:plc:alice/org.hypercerts.helper.workScopeTag/abc123", - cid: "...", + $type: "org.hypercerts.workscope.cel", + expression: + "scope.hasAll(['mangrove_restoration', 'environmental_education']) && location.country == 'KE'", + usedTags: [ + { + uri: "at://did:plc:alice/org.hypercerts.workscope.tag/3k2abc", + cid: "...", + }, + { + uri: "at://did:plc:alice/org.hypercerts.workscope.tag/7x9def", + cid: "...", + }, + ], + version: "v1", + createdAt: new Date().toISOString(), }, - // OR a simple string: workScope: "Environmental conservation", + // OR a strongRef to a single work scope tag: + // workScope: { uri: "at://did:plc:alice/org.hypercerts.workscope.tag/abc123", cid: "..." }, + // OR a simple string: workScope: { $type: "org.hypercerts.claim.activity#workScopeString", scope: "Environmental conservation" }, startDate: "2023-01-01T00:00:00Z", endDate: "2023-12-31T23:59:59Z", createdAt: new Date().toISOString(), diff --git a/SCHEMAS.md b/SCHEMAS.md index f521cfe6..064f43fa 100644 --- a/SCHEMAS.md +++ b/SCHEMAS.md @@ -15,20 +15,20 @@ Hypercerts-specific lexicons for tracking impact work and claims. #### Properties -| Property | Type | Required | Description | Comments | -| ------------------------ | -------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | -| `title` | `string` | ✅ | Display title summarizing the impact work (e.g. 'Reforestation in Amazon Basin 2024') | maxLength: 256 | -| `shortDescription` | `string` | ✅ | Short summary of this activity claim, suitable for previews and list views. Rich text annotations may be provided via `shortDescriptionFacets`. | maxLength: 3000, maxGraphemes: 300 | -| `shortDescriptionFacets` | `ref[]` | ❌ | Rich text annotations for `shortDescription` (mentions, URLs, hashtags, etc). | | -| `description` | `ref` | ❌ | Rich-text description, represented as a Leaflet linear document. | | -| `image` | `union` | ❌ | The hypercert visual representation as a URI or image blob. | | -| `contributors` | `ref[]` | ❌ | An array of contributor objects, each containing contributor information, weight, and contribution details. | maxLength: 1000 | -| `workScope` | `union` | ❌ | Work scope definition. Either a strongRef to a work-scope logic record (structured, nested logic), or a free-form string for simple or legacy scopes. The work scope record should conform to the org.hypercerts.helper.workScopeTag lexicon. | | -| `startDate` | `string` | ❌ | When the work began | | -| `endDate` | `string` | ❌ | When the work ended | | -| `locations` | `ref[]` | ❌ | An array of strong references to the location where activity was performed. The record referenced must conform with the lexicon app.certified.location. | maxLength: 1000 | -| `rights` | `ref` | ❌ | A strong reference to the rights that this hypercert has. The record referenced must conform with the lexicon org.hypercerts.claim.rights. | | -| `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created | | +| Property | Type | Required | Description | Comments | +| ------------------------ | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | +| `title` | `string` | ✅ | Display title summarizing the impact work (e.g. 'Reforestation in Amazon Basin 2024') | maxLength: 256 | +| `shortDescription` | `string` | ✅ | Short summary of this activity claim, suitable for previews and list views. Rich text annotations may be provided via `shortDescriptionFacets`. | maxLength: 3000, maxGraphemes: 300 | +| `shortDescriptionFacets` | `ref[]` | ❌ | Rich text annotations for `shortDescription` (mentions, URLs, hashtags, etc). | | +| `description` | `ref` | ❌ | Rich-text description, represented as a Leaflet linear document. | | +| `image` | `union` | ❌ | The hypercert visual representation as a URI or image blob. | | +| `contributors` | `ref[]` | ❌ | An array of contributor objects, each containing contributor information, weight, and contribution details. | maxLength: 1000 | +| `workScope` | `union` | ❌ | Work scope definition. A CEL expression for structured, machine-evaluable scopes or a free-form string for simple and legacy scopes. | | +| `startDate` | `string` | ❌ | When the work began | | +| `endDate` | `string` | ❌ | When the work ended | | +| `locations` | `ref[]` | ❌ | An array of strong references to the location where activity was performed. The record referenced must conform with the lexicon app.certified.location. | maxLength: 1000 | +| `rights` | `ref` | ❌ | A strong reference to the rights that this hypercert has. The record referenced must conform with the lexicon org.hypercerts.claim.rights. | | +| `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created | | #### Defs @@ -266,24 +266,42 @@ Hypercerts-specific lexicons for tracking impact work and claims. --- -### `org.hypercerts.helper.workScopeTag` +### `org.hypercerts.workscope.cel` -**Description:** A reusable scope atom for work scope logic expressions. Scopes can represent topics, languages, domains, deliverables, methods, regions, tags, or other categorical labels. +**Description:** A structured, machine-evaluable work scope definition using CEL (Common Expression Language). Tags referenced in the expression correspond to org.hypercerts.workscope.tag keys. See https://github.com/google/cel-spec. Note: this is intentionally type 'object' (not 'record') so it can be directly embedded inline in union types (e.g., activity.workScope) without requiring a separate collection or strongRef indirection. + +#### Properties + +| Property | Type | Required | Description | Comments | +| ------------ | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | +| `expression` | `string` | ✅ | A CEL expression encoding the work scope conditions. Example: scope.hasAll(['mangrove_restoration', 'environmental_education']) && location.country == 'KE' | maxLength: 10000, maxGraphemes: 5000 | +| `usedTags` | `ref[]` | ✅ | Strong references to org.hypercerts.workscope.tag records used in the expression. Enables fast indexing by AT-URI and provides referential integrity to the underlying tag records. | maxLength: 100 | +| `version` | `string` | ✅ | CEL context schema version. | maxLength: 16, Known values: `v1` | +| `createdAt` | `string` | ✅ | Client-declared timestamp when this expression was originally created. | | + +--- + +### `org.hypercerts.workscope.tag` + +**Description:** A reusable scope atom for work scope logic expressions. Scopes can represent topics, languages, domains, deliverables, methods, regions, tags, or other categorical labels. Tags are composed into structured expressions via CEL (Common Expression Language) on activity records. **Key:** `tid` #### Properties -| Property | Type | Required | Description | Comments | -| ------------------- | ---------- | -------- | ------------------------------------------------------------------------------------------------ | ------------------------------------ | -| `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created | | -| `key` | `string` | ✅ | Lowercase, hyphenated machine-readable key for this scope (e.g., 'ipfs', 'go-lang', 'filecoin'). | maxLength: 120 | -| `label` | `string` | ✅ | Human-readable display label for this scope (e.g. 'IPFS', 'Go Programming', 'Climate Action') | maxLength: 200 | -| `kind` | `string` | ❌ | Category type of this scope. Recommended values: topic, language, domain, method, tag. | maxLength: 50 | -| `description` | `string` | ❌ | Optional longer description of this scope. | maxLength: 10000, maxGraphemes: 1000 | -| `parent` | `ref` | ❌ | Optional strong reference to a parent scope record for taxonomy/hierarchy support. | | -| `aliases` | `string[]` | ❌ | Optional array of alternative names or identifiers for this scope. | maxLength: 50 | -| `externalReference` | `union` | ❌ | Optional external reference for this scope as a URI or blob. | | +| Property | Type | Required | Description | Comments | +| ------------------- | ---------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | +| `key` | `string` | ✅ | Lowercase, underscore-separated machine-readable key for this scope (e.g., 'mangrove_restoration', 'biodiversity_monitoring'). Used as the canonical identifier in CEL expressions. | maxLength: 120 | +| `name` | `string` | ✅ | Human-readable name for this scope. | maxLength: 200 | +| `category` | `string` | ❌ | Category type of this scope. | maxLength: 50, Known values: `topic`, `language`, `domain`, `method` | +| `description` | `string` | ❌ | Optional longer description of this scope. | maxLength: 10000, maxGraphemes: 1000 | +| `parent` | `ref` | ❌ | Optional strong reference to a parent work scope tag record for taxonomy/hierarchy support. The record referenced must conform with the lexicon org.hypercerts.workscope.tag. | | +| `status` | `string` | ❌ | Lifecycle status of this tag. Communities propose tags, curators accept them, deprecated tags point to replacements via supersededBy. | maxLength: 20, Known values: `proposed`, `accepted`, `deprecated` | +| `supersededBy` | `ref` | ❌ | When status is 'deprecated', points to the replacement work scope tag record. The record referenced must conform with the lexicon org.hypercerts.workscope.tag. | | +| `aliases` | `string[]` | ❌ | Alternative human-readable names for this scope (e.g., translations, abbreviations, or common synonyms). Unlike sameAs, these are plain-text labels, not links to external ontologies. | maxLength: 50 | +| `sameAs` | `string[]` | ❌ | URIs to semantically equivalent concepts in external ontologies or taxonomies (e.g., Wikidata QIDs, ENVO terms, SDG targets). Used for interoperability, not as documentation. | maxLength: 20 | +| `referenceDocument` | `union` | ❌ | Link to a governance or reference document where this work scope tag is defined and further explained. | | +| `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created. | | --- @@ -482,7 +500,12 @@ External lexicons from other protocols and systems. ### `com.atproto.repo.strongRef` -**Key:** `tid` +#### Properties + +| Property | Type | Required | Description | +| -------- | -------- | -------- | ----------- | +| `uri` | `string` | ✅ | | +| `cid` | `string` | ✅ | | --- diff --git a/lexicons/org/hypercerts/claim/activity.json b/lexicons/org/hypercerts/claim/activity.json index 5f2b26d1..b5cc92bb 100644 --- a/lexicons/org/hypercerts/claim/activity.json +++ b/lexicons/org/hypercerts/claim/activity.json @@ -53,8 +53,8 @@ }, "workScope": { "type": "union", - "refs": ["com.atproto.repo.strongRef", "#workScopeString"], - "description": "Work scope definition. Either a strongRef to a work-scope logic record (structured, nested logic), or a free-form string for simple or legacy scopes. The work scope record should conform to the org.hypercerts.helper.workScopeTag lexicon." + "refs": ["org.hypercerts.workscope.cel", "#workScopeString"], + "description": "Work scope definition. A CEL expression for structured, machine-evaluable scopes or a free-form string for simple and legacy scopes." }, "startDate": { "type": "string", diff --git a/lexicons/org/hypercerts/helper/workScopeTag.json b/lexicons/org/hypercerts/helper/workScopeTag.json deleted file mode 100644 index 3747a430..00000000 --- a/lexicons/org/hypercerts/helper/workScopeTag.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "lexicon": 1, - "id": "org.hypercerts.helper.workScopeTag", - "defs": { - "main": { - "type": "record", - "description": "A reusable scope atom for work scope logic expressions. Scopes can represent topics, languages, domains, deliverables, methods, regions, tags, or other categorical labels.", - "key": "tid", - "record": { - "type": "object", - "required": ["createdAt", "key", "label"], - "properties": { - "createdAt": { - "type": "string", - "format": "datetime", - "description": "Client-declared timestamp when this record was originally created" - }, - "key": { - "type": "string", - "description": "Lowercase, hyphenated machine-readable key for this scope (e.g., 'ipfs', 'go-lang', 'filecoin').", - "maxLength": 120 - }, - "label": { - "type": "string", - "description": "Human-readable display label for this scope (e.g. 'IPFS', 'Go Programming', 'Climate Action')", - "maxLength": 200 - }, - "kind": { - "type": "string", - "description": "Category type of this scope. Recommended values: topic, language, domain, method, tag.", - "maxLength": 50 - }, - "description": { - "type": "string", - "description": "Optional longer description of this scope.", - "maxLength": 10000, - "maxGraphemes": 1000 - }, - "parent": { - "type": "ref", - "ref": "com.atproto.repo.strongRef", - "description": "Optional strong reference to a parent scope record for taxonomy/hierarchy support." - }, - "aliases": { - "type": "array", - "items": { - "type": "string", - "maxLength": 200 - }, - "maxLength": 50, - "description": "Optional array of alternative names or identifiers for this scope." - }, - "externalReference": { - "type": "union", - "refs": [ - "org.hypercerts.defs#uri", - "org.hypercerts.defs#smallBlob" - ], - "description": "Optional external reference for this scope as a URI or blob." - } - } - } - } - } -} diff --git a/lexicons/org/hypercerts/workscope/cel.json b/lexicons/org/hypercerts/workscope/cel.json new file mode 100644 index 00000000..d3f72dce --- /dev/null +++ b/lexicons/org/hypercerts/workscope/cel.json @@ -0,0 +1,39 @@ +{ + "lexicon": 1, + "id": "org.hypercerts.workscope.cel", + "defs": { + "main": { + "type": "object", + "description": "A structured, machine-evaluable work scope definition using CEL (Common Expression Language). Tags referenced in the expression correspond to org.hypercerts.workscope.tag keys. See https://github.com/google/cel-spec. Note: this is intentionally type 'object' (not 'record') so it can be directly embedded inline in union types (e.g., activity.workScope) without requiring a separate collection or strongRef indirection.", + "required": ["expression", "usedTags", "version", "createdAt"], + "properties": { + "expression": { + "type": "string", + "maxLength": 10000, + "maxGraphemes": 5000, + "description": "A CEL expression encoding the work scope conditions. Example: scope.hasAll(['mangrove_restoration', 'environmental_education']) && location.country == 'KE'" + }, + "usedTags": { + "type": "array", + "items": { + "type": "ref", + "ref": "com.atproto.repo.strongRef" + }, + "maxLength": 100, + "description": "Strong references to org.hypercerts.workscope.tag records used in the expression. Enables fast indexing by AT-URI and provides referential integrity to the underlying tag records." + }, + "version": { + "type": "string", + "maxLength": 16, + "knownValues": ["v1"], + "description": "CEL context schema version." + }, + "createdAt": { + "type": "string", + "format": "datetime", + "description": "Client-declared timestamp when this expression was originally created." + } + } + } + } +} diff --git a/lexicons/org/hypercerts/workscope/tag.json b/lexicons/org/hypercerts/workscope/tag.json new file mode 100644 index 00000000..8aa9203d --- /dev/null +++ b/lexicons/org/hypercerts/workscope/tag.json @@ -0,0 +1,87 @@ +{ + "lexicon": 1, + "id": "org.hypercerts.workscope.tag", + "defs": { + "main": { + "type": "record", + "description": "A reusable scope atom for work scope logic expressions. Scopes can represent topics, languages, domains, deliverables, methods, regions, tags, or other categorical labels. Tags are composed into structured expressions via CEL (Common Expression Language) on activity records.", + "key": "tid", + "record": { + "type": "object", + "required": ["key", "name", "createdAt"], + "properties": { + "key": { + "type": "string", + "description": "Lowercase, underscore-separated machine-readable key for this scope (e.g., 'mangrove_restoration', 'biodiversity_monitoring'). Used as the canonical identifier in CEL expressions.", + "maxLength": 120 + }, + "name": { + "type": "string", + "description": "Human-readable name for this scope.", + "maxLength": 200 + }, + "category": { + "type": "string", + "description": "Category type of this scope.", + "knownValues": ["topic", "language", "domain", "method"], + "maxLength": 50 + }, + "description": { + "type": "string", + "description": "Optional longer description of this scope.", + "maxLength": 10000, + "maxGraphemes": 1000 + }, + "parent": { + "type": "ref", + "ref": "com.atproto.repo.strongRef", + "description": "Optional strong reference to a parent work scope tag record for taxonomy/hierarchy support. The record referenced must conform with the lexicon org.hypercerts.workscope.tag." + }, + "status": { + "type": "string", + "description": "Lifecycle status of this tag. Communities propose tags, curators accept them, deprecated tags point to replacements via supersededBy.", + "knownValues": ["proposed", "accepted", "deprecated"], + "maxLength": 20 + }, + "supersededBy": { + "type": "ref", + "ref": "com.atproto.repo.strongRef", + "description": "When status is 'deprecated', points to the replacement work scope tag record. The record referenced must conform with the lexicon org.hypercerts.workscope.tag." + }, + "aliases": { + "type": "array", + "items": { + "type": "string", + "maxLength": 200 + }, + "maxLength": 50, + "description": "Alternative human-readable names for this scope (e.g., translations, abbreviations, or common synonyms). Unlike sameAs, these are plain-text labels, not links to external ontologies." + }, + "sameAs": { + "type": "array", + "items": { + "type": "string", + "format": "uri", + "maxLength": 2048 + }, + "maxLength": 20, + "description": "URIs to semantically equivalent concepts in external ontologies or taxonomies (e.g., Wikidata QIDs, ENVO terms, SDG targets). Used for interoperability, not as documentation." + }, + "referenceDocument": { + "type": "union", + "refs": [ + "org.hypercerts.defs#uri", + "org.hypercerts.defs#smallBlob" + ], + "description": "Link to a governance or reference document where this work scope tag is defined and further explained." + }, + "createdAt": { + "type": "string", + "format": "datetime", + "description": "Client-declared timestamp when this record was originally created." + } + } + } + } + } +} diff --git a/scripts/generate-schemas.js b/scripts/generate-schemas.js index 06fcb706..cbd82c72 100644 --- a/scripts/generate-schemas.js +++ b/scripts/generate-schemas.js @@ -286,19 +286,21 @@ function generateMainSection(mainDef, lexicon) { output.push(...generateDescription(mainDef.description)); - // Determine key type - const keyType = mainDef.key || "tid"; - output.push(`**Key:** \`${keyType}\``, ""); + // Object types (e.g., celExpression) have no record key + if (mainDef.type === "record") { + const keyType = mainDef.key || "tid"; + output.push(`**Key:** \`${keyType}\``, ""); + } + + // Determine where properties live: record types nest under main.record, + // object types have properties directly on main + const propsSource = mainDef.record || mainDef; + const hasProperties = propsSource.properties !== undefined; - // Standard properties table - if (mainDef.record) { + if (hasProperties) { output.push("#### Properties", ""); - const required = mainDef.record.required || []; - const rows = extractPropertyRows( - mainDef.record, - required, - lexicon.data.defs, - ); + const required = propsSource.required || []; + const rows = extractPropertyRows(propsSource, required, lexicon.data.defs); if (rows.length > 0) { const hasComments = rows.some((r) => r.comments); @@ -306,7 +308,7 @@ function generateMainSection(mainDef, lexicon) { } } - return { output, hasProperties: mainDef.record?.properties !== undefined }; + return { output, hasProperties }; } function generateAdditionalDefsSection(lexicon, hasPropertiesBefore = false) {