Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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-cel-work-scope-expressions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hypercerts-org/lexicon": minor
---

Add CEL expression support for structured work scopes (`org.hypercerts.workscope.cel`, `org.hypercerts.workscope.tag`)
33 changes: 33 additions & 0 deletions ERD.puml
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,33 @@ dataclass badgeResponse {
!endif
}

' org.hypercerts.ontology.celExpression
dataclass celExpression {
!if (SHOW_FIELDS == "true")
expression
usedTags[]
version
createdAt
!endif
}

' org.hypercerts.ontology.workScopeTag
dataclass workScopeTag {
!if (SHOW_FIELDS == "true")
key
label
kind?
description?
parent?
status?
supersededBy?
aliases[]?
sameAs[]?
externalReference?
createdAt
!endif
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

' org.hypercerts.claim.rights
dataclass rights {
!if (SHOW_FIELDS == "true")
Expand Down Expand Up @@ -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
Expand Down
23 changes: 19 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: "...#workScopeString", scope: "Environmental conservation" },
startDate: "2023-01-01T00:00:00Z",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
endDate: "2023-12-31T23:59:59Z",
createdAt: new Date().toISOString(),
Expand Down
77 changes: 50 additions & 27 deletions SCHEMAS.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions lexicons/org/hypercerts/claim/activity.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
65 changes: 0 additions & 65 deletions lexicons/org/hypercerts/helper/workScopeTag.json

This file was deleted.

39 changes: 39 additions & 0 deletions lexicons/org/hypercerts/workscope/cel.json
Original file line number Diff line number Diff line change
@@ -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."
}
}
}
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
87 changes: 87 additions & 0 deletions lexicons/org/hypercerts/workscope/tag.json
Original file line number Diff line number Diff line change
@@ -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."
}
}
}
}
}
}
26 changes: 14 additions & 12 deletions scripts/generate-schemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -286,27 +286,29 @@ 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);
output.push(...renderTable(rows, hasComments));
}
}

return { output, hasProperties: mainDef.record?.properties !== undefined };
return { output, hasProperties };
}

function generateAdditionalDefsSection(lexicon, hasPropertiesBefore = false) {
Expand Down