From d0953ef3047b46dfbad0dd983eff3272ab5613bf Mon Sep 17 00:00:00 2001 From: kzoeps Date: Wed, 4 Mar 2026 18:20:56 +0600 Subject: [PATCH 1/5] feat: add leaflet and richtext lexicons --- .changeset/add-pub-leaflet-lexicons.md | 5 + lexicons/app/bsky/richtext/facet.json | 51 +++++++ lexicons/pub/leaflet/blocks/blockquote.json | 22 +++ lexicons/pub/leaflet/blocks/bskyPost.json | 19 +++ lexicons/pub/leaflet/blocks/button.json | 19 +++ lexicons/pub/leaflet/blocks/code.json | 21 +++ lexicons/pub/leaflet/blocks/header.json | 27 ++++ .../pub/leaflet/blocks/horizontalRule.json | 11 ++ lexicons/pub/leaflet/blocks/iframe.json | 21 +++ lexicons/pub/leaflet/blocks/image.json | 37 +++++ lexicons/pub/leaflet/blocks/math.json | 15 ++ lexicons/pub/leaflet/blocks/orderedList.json | 50 +++++++ lexicons/pub/leaflet/blocks/page.json | 15 ++ lexicons/pub/leaflet/blocks/poll.json | 16 +++ lexicons/pub/leaflet/blocks/text.json | 26 ++++ .../pub/leaflet/blocks/unorderedList.json | 46 +++++++ lexicons/pub/leaflet/blocks/website.json | 27 ++++ .../pub/leaflet/pages/linearDocument.json | 98 ++++++++++++++ lexicons/pub/leaflet/richtext/facet.json | 128 ++++++++++++++++++ 19 files changed, 654 insertions(+) create mode 100644 .changeset/add-pub-leaflet-lexicons.md create mode 100644 lexicons/app/bsky/richtext/facet.json create mode 100644 lexicons/pub/leaflet/blocks/blockquote.json create mode 100644 lexicons/pub/leaflet/blocks/bskyPost.json create mode 100644 lexicons/pub/leaflet/blocks/button.json create mode 100644 lexicons/pub/leaflet/blocks/code.json create mode 100644 lexicons/pub/leaflet/blocks/header.json create mode 100644 lexicons/pub/leaflet/blocks/horizontalRule.json create mode 100644 lexicons/pub/leaflet/blocks/iframe.json create mode 100644 lexicons/pub/leaflet/blocks/image.json create mode 100644 lexicons/pub/leaflet/blocks/math.json create mode 100644 lexicons/pub/leaflet/blocks/orderedList.json create mode 100644 lexicons/pub/leaflet/blocks/page.json create mode 100644 lexicons/pub/leaflet/blocks/poll.json create mode 100644 lexicons/pub/leaflet/blocks/text.json create mode 100644 lexicons/pub/leaflet/blocks/unorderedList.json create mode 100644 lexicons/pub/leaflet/blocks/website.json create mode 100644 lexicons/pub/leaflet/pages/linearDocument.json create mode 100644 lexicons/pub/leaflet/richtext/facet.json diff --git a/.changeset/add-pub-leaflet-lexicons.md b/.changeset/add-pub-leaflet-lexicons.md new file mode 100644 index 00000000..328c9567 --- /dev/null +++ b/.changeset/add-pub-leaflet-lexicons.md @@ -0,0 +1,5 @@ +--- +"@hypercerts-org/lexicon": minor +--- + +Add pub.leaflet block, page, and richtext lexicons vendored from the Leaflet protocol diff --git a/lexicons/app/bsky/richtext/facet.json b/lexicons/app/bsky/richtext/facet.json new file mode 100644 index 00000000..388a3a5e --- /dev/null +++ b/lexicons/app/bsky/richtext/facet.json @@ -0,0 +1,51 @@ +{ + "lexicon": 1, + "id": "app.bsky.richtext.facet", + "defs": { + "main": { + "type": "object", + "description": "Annotation of a sub-string within rich text.", + "required": ["index", "features"], + "properties": { + "index": { "type": "ref", "ref": "#byteSlice" }, + "features": { + "type": "array", + "items": { "type": "union", "refs": ["#mention", "#link", "#tag"] } + } + } + }, + "mention": { + "type": "object", + "description": "Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID.", + "required": ["did"], + "properties": { + "did": { "type": "string", "format": "did" } + } + }, + "link": { + "type": "object", + "description": "Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL.", + "required": ["uri"], + "properties": { + "uri": { "type": "string", "format": "uri" } + } + }, + "tag": { + "type": "object", + "description": "Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags').", + "required": ["tag"], + "properties": { + "tag": { "type": "string", "maxLength": 640, "maxGraphemes": 64 } + } + }, + "byteSlice": { + "type": "object", + "description": "Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets.", + "required": ["byteStart", "byteEnd"], + "properties": { + "byteStart": { "type": "integer", "minimum": 0 }, + "byteEnd": { "type": "integer", "minimum": 0 } + } + } + } +} diff --git a/lexicons/pub/leaflet/blocks/blockquote.json b/lexicons/pub/leaflet/blocks/blockquote.json new file mode 100644 index 00000000..1f033a3d --- /dev/null +++ b/lexicons/pub/leaflet/blocks/blockquote.json @@ -0,0 +1,22 @@ +{ + "lexicon": 1, + "id": "pub.leaflet.blocks.blockquote", + "defs": { + "main": { + "type": "object", + "required": ["plaintext"], + "properties": { + "plaintext": { + "type": "string" + }, + "facets": { + "type": "array", + "items": { + "type": "ref", + "ref": "pub.leaflet.richtext.facet" + } + } + } + } + } +} diff --git a/lexicons/pub/leaflet/blocks/bskyPost.json b/lexicons/pub/leaflet/blocks/bskyPost.json new file mode 100644 index 00000000..691aa76a --- /dev/null +++ b/lexicons/pub/leaflet/blocks/bskyPost.json @@ -0,0 +1,19 @@ +{ + "lexicon": 1, + "id": "pub.leaflet.blocks.bskyPost", + "defs": { + "main": { + "type": "object", + "required": ["postRef"], + "properties": { + "postRef": { + "type": "ref", + "ref": "com.atproto.repo.strongRef" + }, + "clientHost": { + "type": "string" + } + } + } + } +} diff --git a/lexicons/pub/leaflet/blocks/button.json b/lexicons/pub/leaflet/blocks/button.json new file mode 100644 index 00000000..96a31cf5 --- /dev/null +++ b/lexicons/pub/leaflet/blocks/button.json @@ -0,0 +1,19 @@ +{ + "lexicon": 1, + "id": "pub.leaflet.blocks.button", + "defs": { + "main": { + "type": "object", + "required": ["text", "url"], + "properties": { + "text": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + } + } + } +} diff --git a/lexicons/pub/leaflet/blocks/code.json b/lexicons/pub/leaflet/blocks/code.json new file mode 100644 index 00000000..c0e1235b --- /dev/null +++ b/lexicons/pub/leaflet/blocks/code.json @@ -0,0 +1,21 @@ +{ + "lexicon": 1, + "id": "pub.leaflet.blocks.code", + "defs": { + "main": { + "type": "object", + "required": ["plaintext"], + "properties": { + "plaintext": { + "type": "string" + }, + "language": { + "type": "string" + }, + "syntaxHighlightingTheme": { + "type": "string" + } + } + } + } +} diff --git a/lexicons/pub/leaflet/blocks/header.json b/lexicons/pub/leaflet/blocks/header.json new file mode 100644 index 00000000..961d61ce --- /dev/null +++ b/lexicons/pub/leaflet/blocks/header.json @@ -0,0 +1,27 @@ +{ + "lexicon": 1, + "id": "pub.leaflet.blocks.header", + "defs": { + "main": { + "type": "object", + "required": ["plaintext"], + "properties": { + "level": { + "type": "integer", + "minimum": 1, + "maximum": 6 + }, + "plaintext": { + "type": "string" + }, + "facets": { + "type": "array", + "items": { + "type": "ref", + "ref": "pub.leaflet.richtext.facet" + } + } + } + } + } +} diff --git a/lexicons/pub/leaflet/blocks/horizontalRule.json b/lexicons/pub/leaflet/blocks/horizontalRule.json new file mode 100644 index 00000000..c1941409 --- /dev/null +++ b/lexicons/pub/leaflet/blocks/horizontalRule.json @@ -0,0 +1,11 @@ +{ + "lexicon": 1, + "id": "pub.leaflet.blocks.horizontalRule", + "defs": { + "main": { + "type": "object", + "required": [], + "properties": {} + } + } +} diff --git a/lexicons/pub/leaflet/blocks/iframe.json b/lexicons/pub/leaflet/blocks/iframe.json new file mode 100644 index 00000000..7b3e8b24 --- /dev/null +++ b/lexicons/pub/leaflet/blocks/iframe.json @@ -0,0 +1,21 @@ +{ + "lexicon": 1, + "id": "pub.leaflet.blocks.iframe", + "defs": { + "main": { + "type": "object", + "required": ["url"], + "properties": { + "url": { + "type": "string", + "format": "uri" + }, + "height": { + "type": "integer", + "minimum": 16, + "maximum": 1600 + } + } + } + } +} diff --git a/lexicons/pub/leaflet/blocks/image.json b/lexicons/pub/leaflet/blocks/image.json new file mode 100644 index 00000000..88ad667a --- /dev/null +++ b/lexicons/pub/leaflet/blocks/image.json @@ -0,0 +1,37 @@ +{ + "lexicon": 1, + "id": "pub.leaflet.blocks.image", + "defs": { + "main": { + "type": "object", + "required": ["image", "aspectRatio"], + "properties": { + "image": { + "type": "blob", + "accept": ["image/*"], + "maxSize": 1000000 + }, + "alt": { + "type": "string", + "description": "Alt text description of the image, for accessibility." + }, + "aspectRatio": { + "type": "ref", + "ref": "#aspectRatio" + } + } + }, + "aspectRatio": { + "type": "object", + "required": ["width", "height"], + "properties": { + "width": { + "type": "integer" + }, + "height": { + "type": "integer" + } + } + } + } +} diff --git a/lexicons/pub/leaflet/blocks/math.json b/lexicons/pub/leaflet/blocks/math.json new file mode 100644 index 00000000..1e50f0e3 --- /dev/null +++ b/lexicons/pub/leaflet/blocks/math.json @@ -0,0 +1,15 @@ +{ + "lexicon": 1, + "id": "pub.leaflet.blocks.math", + "defs": { + "main": { + "type": "object", + "required": ["tex"], + "properties": { + "tex": { + "type": "string" + } + } + } + } +} diff --git a/lexicons/pub/leaflet/blocks/orderedList.json b/lexicons/pub/leaflet/blocks/orderedList.json new file mode 100644 index 00000000..3c1fe839 --- /dev/null +++ b/lexicons/pub/leaflet/blocks/orderedList.json @@ -0,0 +1,50 @@ +{ + "lexicon": 1, + "id": "pub.leaflet.blocks.orderedList", + "defs": { + "main": { + "type": "object", + "required": ["children"], + "properties": { + "startIndex": { + "type": "integer", + "description": "The starting number for this ordered list. Defaults to 1 if not specified." + }, + "children": { + "type": "array", + "items": { + "type": "ref", + "ref": "#listItem" + } + } + } + }, + "listItem": { + "type": "object", + "required": ["content"], + "properties": { + "content": { + "type": "union", + "refs": [ + "pub.leaflet.blocks.text", + "pub.leaflet.blocks.header", + "pub.leaflet.blocks.image" + ] + }, + "children": { + "type": "array", + "description": "Nested ordered list items. Mutually exclusive with unorderedListChildren; if both are present, children takes precedence.", + "items": { + "type": "ref", + "ref": "#listItem" + } + }, + "unorderedListChildren": { + "type": "ref", + "description": "A nested unordered list. Mutually exclusive with children; if both are present, children takes precedence.", + "ref": "pub.leaflet.blocks.unorderedList" + } + } + } + } +} diff --git a/lexicons/pub/leaflet/blocks/page.json b/lexicons/pub/leaflet/blocks/page.json new file mode 100644 index 00000000..40c22ee8 --- /dev/null +++ b/lexicons/pub/leaflet/blocks/page.json @@ -0,0 +1,15 @@ +{ + "lexicon": 1, + "id": "pub.leaflet.blocks.page", + "defs": { + "main": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "string" + } + } + } + } +} diff --git a/lexicons/pub/leaflet/blocks/poll.json b/lexicons/pub/leaflet/blocks/poll.json new file mode 100644 index 00000000..f0dce1d3 --- /dev/null +++ b/lexicons/pub/leaflet/blocks/poll.json @@ -0,0 +1,16 @@ +{ + "lexicon": 1, + "id": "pub.leaflet.blocks.poll", + "defs": { + "main": { + "type": "object", + "required": ["pollRef"], + "properties": { + "pollRef": { + "type": "ref", + "ref": "com.atproto.repo.strongRef" + } + } + } + } +} diff --git a/lexicons/pub/leaflet/blocks/text.json b/lexicons/pub/leaflet/blocks/text.json new file mode 100644 index 00000000..67fcdd18 --- /dev/null +++ b/lexicons/pub/leaflet/blocks/text.json @@ -0,0 +1,26 @@ +{ + "lexicon": 1, + "id": "pub.leaflet.blocks.text", + "defs": { + "main": { + "type": "object", + "required": ["plaintext"], + "properties": { + "plaintext": { + "type": "string" + }, + "textSize": { + "type": "string", + "enum": ["default", "small", "large"] + }, + "facets": { + "type": "array", + "items": { + "type": "ref", + "ref": "pub.leaflet.richtext.facet" + } + } + } + } + } +} diff --git a/lexicons/pub/leaflet/blocks/unorderedList.json b/lexicons/pub/leaflet/blocks/unorderedList.json new file mode 100644 index 00000000..fdb408d3 --- /dev/null +++ b/lexicons/pub/leaflet/blocks/unorderedList.json @@ -0,0 +1,46 @@ +{ + "lexicon": 1, + "id": "pub.leaflet.blocks.unorderedList", + "defs": { + "main": { + "type": "object", + "required": ["children"], + "properties": { + "children": { + "type": "array", + "items": { + "type": "ref", + "ref": "#listItem" + } + } + } + }, + "listItem": { + "type": "object", + "required": ["content"], + "properties": { + "content": { + "type": "union", + "refs": [ + "pub.leaflet.blocks.text", + "pub.leaflet.blocks.header", + "pub.leaflet.blocks.image" + ] + }, + "children": { + "type": "array", + "description": "Nested unordered list items. Mutually exclusive with orderedListChildren; if both are present, children takes precedence.", + "items": { + "type": "ref", + "ref": "#listItem" + } + }, + "orderedListChildren": { + "type": "ref", + "description": "Nested ordered list items. Mutually exclusive with children; if both are present, children takes precedence.", + "ref": "pub.leaflet.blocks.orderedList" + } + } + } + } +} diff --git a/lexicons/pub/leaflet/blocks/website.json b/lexicons/pub/leaflet/blocks/website.json new file mode 100644 index 00000000..3308b5ff --- /dev/null +++ b/lexicons/pub/leaflet/blocks/website.json @@ -0,0 +1,27 @@ +{ + "lexicon": 1, + "id": "pub.leaflet.blocks.website", + "defs": { + "main": { + "type": "object", + "required": ["src"], + "properties": { + "previewImage": { + "type": "blob", + "accept": ["image/*"], + "maxSize": 1000000 + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "src": { + "type": "string", + "format": "uri" + } + } + } + } +} diff --git a/lexicons/pub/leaflet/pages/linearDocument.json b/lexicons/pub/leaflet/pages/linearDocument.json new file mode 100644 index 00000000..a8092083 --- /dev/null +++ b/lexicons/pub/leaflet/pages/linearDocument.json @@ -0,0 +1,98 @@ +{ + "lexicon": 1, + "id": "pub.leaflet.pages.linearDocument", + "defs": { + "main": { + "type": "object", + "required": ["blocks"], + "properties": { + "id": { + "type": "string" + }, + "blocks": { + "type": "array", + "items": { + "type": "ref", + "ref": "#block" + } + } + } + }, + "block": { + "type": "object", + "required": ["block"], + "properties": { + "block": { + "type": "union", + "refs": [ + "pub.leaflet.blocks.iframe", + "pub.leaflet.blocks.text", + "pub.leaflet.blocks.blockquote", + "pub.leaflet.blocks.header", + "pub.leaflet.blocks.image", + "pub.leaflet.blocks.unorderedList", + "pub.leaflet.blocks.orderedList", + "pub.leaflet.blocks.website", + "pub.leaflet.blocks.math", + "pub.leaflet.blocks.code", + "pub.leaflet.blocks.horizontalRule", + "pub.leaflet.blocks.bskyPost", + "pub.leaflet.blocks.page", + "pub.leaflet.blocks.poll", + "pub.leaflet.blocks.button" + ] + }, + "alignment": { + "type": "string", + "knownValues": [ + "#textAlignLeft", + "#textAlignCenter", + "#textAlignRight", + "#textAlignJustify" + ] + } + } + }, + "textAlignLeft": { + "type": "token" + }, + "textAlignCenter": { + "type": "token" + }, + "textAlignRight": { + "type": "token" + }, + "textAlignJustify": { + "type": "token" + }, + "quote": { + "type": "object", + "required": ["start", "end"], + "properties": { + "start": { + "type": "ref", + "ref": "#position" + }, + "end": { + "type": "ref", + "ref": "#position" + } + } + }, + "position": { + "type": "object", + "required": ["block", "offset"], + "properties": { + "block": { + "type": "array", + "items": { + "type": "integer" + } + }, + "offset": { + "type": "integer" + } + } + } + } +} diff --git a/lexicons/pub/leaflet/richtext/facet.json b/lexicons/pub/leaflet/richtext/facet.json new file mode 100644 index 00000000..7e6e12e8 --- /dev/null +++ b/lexicons/pub/leaflet/richtext/facet.json @@ -0,0 +1,128 @@ +{ + "lexicon": 1, + "id": "pub.leaflet.richtext.facet", + "defs": { + "main": { + "type": "object", + "description": "Annotation of a sub-string within rich text.", + "required": ["index", "features"], + "properties": { + "index": { + "type": "ref", + "ref": "#byteSlice" + }, + "features": { + "type": "array", + "items": { + "type": "union", + "refs": [ + "#link", + "#didMention", + "#atMention", + "#code", + "#highlight", + "#underline", + "#strikethrough", + "#id", + "#bold", + "#italic" + ] + } + } + } + }, + "byteSlice": { + "type": "object", + "description": "Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets.", + "required": ["byteStart", "byteEnd"], + "properties": { + "byteStart": { + "type": "integer", + "minimum": 0 + }, + "byteEnd": { + "type": "integer", + "minimum": 0 + } + } + }, + "link": { + "type": "object", + "description": "Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL.", + "required": ["uri"], + "properties": { + "uri": { + "type": "string" + } + } + }, + "didMention": { + "type": "object", + "description": "Facet feature for mentioning a did.", + "required": ["did"], + "properties": { + "did": { + "type": "string", + "format": "did" + } + } + }, + "atMention": { + "type": "object", + "description": "Facet feature for mentioning an AT URI.", + "required": ["atURI"], + "properties": { + "atURI": { + "type": "string", + "format": "uri" + } + } + }, + "code": { + "type": "object", + "description": "Facet feature for inline code.", + "required": [], + "properties": {} + }, + "highlight": { + "type": "object", + "description": "Facet feature for highlighted text.", + "required": [], + "properties": {} + }, + "underline": { + "type": "object", + "description": "Facet feature for underline markup", + "required": [], + "properties": {} + }, + "strikethrough": { + "type": "object", + "description": "Facet feature for strikethrough markup", + "required": [], + "properties": {} + }, + "id": { + "type": "object", + "description": "Facet feature for an identifier. Used for linking to a segment", + "required": [], + "properties": { + "id": { + "type": "string" + } + } + }, + "bold": { + "type": "object", + "description": "Facet feature for bold text", + "required": [], + "properties": {} + }, + "italic": { + "type": "object", + "description": "Facet feature for italic text", + "required": [], + "properties": {} + } + } +} From 8657f1ddc8ce3f35d36e0bc5eb89780694f9aece Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Sat, 4 Apr 2026 16:40:19 +0000 Subject: [PATCH 2/5] fix: enable runtime validation for external lexicon refs - Update vendored leaflet lexicons to match upstream (add footnote facet, checklist support in list blocks) - Remove create-shims.sh and @atcute/{leaflet,bluesky} dependencies; vendored JSONs now provide both types and runtime validation - Fix duplicate export names in generate-exports.js for external lexicon namespaces (BSKY_RICHTEXT_FACET vs LEAFLET_RICHTEXT_FACET) - Add validate-external-lexicons.test.ts covering description and facet validation for activity, collection, attachment, measurement - Update AGENTS.md and docs/AUTO_GENERATION.md to remove shim refs --- .changeset/add-pub-leaflet-lexicons.md | 2 +- AGENTS.md | 9 +- SCHEMAS.md | 365 ++++++++++++++++++ docs/AUTO_GENERATION.md | 5 +- lexicons/pub/leaflet/blocks/orderedList.json | 4 + .../pub/leaflet/blocks/unorderedList.json | 4 + lexicons/pub/leaflet/richtext/facet.json | 23 +- package-lock.json | 65 +--- package.json | 4 +- scripts/create-shims.sh | 26 -- scripts/generate-exports.js | 21 +- tests/validate-external-lexicons.test.ts | 156 ++++++++ 12 files changed, 578 insertions(+), 106 deletions(-) delete mode 100755 scripts/create-shims.sh create mode 100644 tests/validate-external-lexicons.test.ts diff --git a/.changeset/add-pub-leaflet-lexicons.md b/.changeset/add-pub-leaflet-lexicons.md index 328c9567..857926c2 100644 --- a/.changeset/add-pub-leaflet-lexicons.md +++ b/.changeset/add-pub-leaflet-lexicons.md @@ -2,4 +2,4 @@ "@hypercerts-org/lexicon": minor --- -Add pub.leaflet block, page, and richtext lexicons vendored from the Leaflet protocol +Vendor pub.leaflet and app.bsky.richtext.facet lexicon JSONs to enable runtime validation of records with description and facet fields. Previously, these external schemas were only shimmed at the TypeScript type level via @atcute packages, causing LexiconDefNotFoundError at runtime when validating records that populated optional description or facet fields. Removes the now-unnecessary create-shims.sh script and @atcute/leaflet + @atcute/bluesky dependencies. diff --git a/AGENTS.md b/AGENTS.md index 5599c0a9..fa8c2d54 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -97,9 +97,9 @@ npm run gen-api This runs `lex gen-api` on all lexicon JSON files and: -1. Generates TypeScript types in `generated/` -2. Creates type shims for external lexicons -3. Auto-generates `generated/exports.ts` with clean exports +1. Generates TypeScript types in `generated/` (including vendored + external lexicons from `lexicons/pub/` and `lexicons/app/bsky/`) +2. Auto-generates `generated/exports.ts` with clean exports Then to build the distributable bundles: @@ -231,8 +231,7 @@ scripts/ Build and codegen scripts structure 2. Run `npm run gen-api` to: - - Generate types in `generated/` - - Create type shims for external lexicons + - Generate types in `generated/` (including vendored external lexicons) - Auto-generate `generated/exports.ts` with all exports 3. Update `ERD.puml` as appropriate: diff --git a/SCHEMAS.md b/SCHEMAS.md index d885a29e..5876cb23 100644 --- a/SCHEMAS.md +++ b/SCHEMAS.md @@ -574,6 +574,54 @@ Object containing a large image External lexicons from other protocols and systems. +### `app.bsky.richtext.facet` + +**Description:** Annotation of a sub-string within rich text. + +#### Properties + +| Property | Type | Required | Description | +| ---------- | --------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `index` | `ref` | ✅ | Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets. | +| `features` | `union[]` | ✅ | | + +#### Defs + +##### `app.bsky.richtext.facet#mention` + +Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID. + +| Property | Type | Required | Description | +| -------- | -------- | -------- | ----------- | +| `did` | `string` | ✅ | | + +##### `app.bsky.richtext.facet#link` + +Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL. + +| Property | Type | Required | Description | +| -------- | -------- | -------- | ----------- | +| `uri` | `string` | ✅ | | + +##### `app.bsky.richtext.facet#tag` + +Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags'). + +| Property | Type | Required | Description | +| -------- | -------- | -------- | ----------- | +| `tag` | `string` | ✅ | | + +##### `app.bsky.richtext.facet#byteSlice` + +Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets. + +| Property | Type | Required | Description | +| ----------- | --------- | -------- | ----------- | +| `byteStart` | `integer` | ✅ | | +| `byteEnd` | `integer` | ✅ | | + +--- + ### `com.atproto.repo.strongRef` #### Properties @@ -656,6 +704,323 @@ Configuration for a specific contributor within a board. Values serve as fallbac --- +### `pub.leaflet.blocks.blockquote` + +#### Properties + +| Property | Type | Required | Description | +| ----------- | -------- | -------- | ----------- | +| `plaintext` | `string` | ✅ | | +| `facets` | `ref[]` | ❌ | | + +--- + +### `pub.leaflet.blocks.bskyPost` + +#### Properties + +| Property | Type | Required | Description | +| ------------ | -------- | -------- | ----------- | +| `postRef` | `ref` | ✅ | | +| `clientHost` | `string` | ❌ | | + +--- + +### `pub.leaflet.blocks.button` + +#### Properties + +| Property | Type | Required | Description | +| -------- | -------- | -------- | ----------- | +| `text` | `string` | ✅ | | +| `url` | `string` | ✅ | | + +--- + +### `pub.leaflet.blocks.code` + +#### Properties + +| Property | Type | Required | Description | +| ------------------------- | -------- | -------- | ----------- | +| `plaintext` | `string` | ✅ | | +| `language` | `string` | ❌ | | +| `syntaxHighlightingTheme` | `string` | ❌ | | + +--- + +### `pub.leaflet.blocks.header` + +#### Properties + +| Property | Type | Required | Description | +| ----------- | --------- | -------- | ----------- | +| `level` | `integer` | ❌ | | +| `plaintext` | `string` | ✅ | | +| `facets` | `ref[]` | ❌ | | + +--- + +### `pub.leaflet.blocks.horizontalRule` + +#### Properties + +--- + +### `pub.leaflet.blocks.iframe` + +#### Properties + +| Property | Type | Required | Description | +| -------- | --------- | -------- | ----------- | +| `url` | `string` | ✅ | | +| `height` | `integer` | ❌ | | + +--- + +### `pub.leaflet.blocks.image` + +#### Properties + +| Property | Type | Required | Description | Comments | +| ------------- | -------- | -------- | ----------------------------------------------------- | ----------------------------------- | +| `image` | `blob` | ✅ | | maxSize: 1000000, accepts: image/\* | +| `alt` | `string` | ❌ | Alt text description of the image, for accessibility. | | +| `aspectRatio` | `ref` | ✅ | | | + +#### Defs + +##### `pub.leaflet.blocks.image#aspectRatio` + +| Property | Type | Required | Description | +| -------- | --------- | -------- | ----------- | +| `width` | `integer` | ✅ | | +| `height` | `integer` | ✅ | | + +--- + +### `pub.leaflet.blocks.math` + +#### Properties + +| Property | Type | Required | Description | +| -------- | -------- | -------- | ----------- | +| `tex` | `string` | ✅ | | + +--- + +### `pub.leaflet.blocks.orderedList` + +#### Properties + +| Property | Type | Required | Description | +| ------------ | --------- | -------- | -------------------------------------------------------------------------- | +| `startIndex` | `integer` | ❌ | The starting number for this ordered list. Defaults to 1 if not specified. | +| `children` | `ref[]` | ✅ | | + +#### Defs + +##### `pub.leaflet.blocks.orderedList#listItem` + +| Property | Type | Required | Description | +| ----------------------- | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------- | +| `checked` | `boolean` | ❌ | If present, this item is a checklist item. true = checked, false = unchecked. If absent, this is a normal list item. | +| `content` | `union` | ✅ | | +| `children` | `ref[]` | ❌ | Nested ordered list items. Mutually exclusive with unorderedListChildren; if both are present, children takes precedence. | +| `unorderedListChildren` | `ref` | ❌ | A nested unordered list. Mutually exclusive with children; if both are present, children takes precedence. | + +--- + +### `pub.leaflet.blocks.page` + +#### Properties + +| Property | Type | Required | Description | +| -------- | -------- | -------- | ----------- | +| `id` | `string` | ✅ | | + +--- + +### `pub.leaflet.blocks.poll` + +#### Properties + +| Property | Type | Required | Description | +| --------- | ----- | -------- | ----------- | +| `pollRef` | `ref` | ✅ | | + +--- + +### `pub.leaflet.blocks.text` + +#### Properties + +| Property | Type | Required | Description | +| ----------- | -------- | -------- | ----------- | +| `plaintext` | `string` | ✅ | | +| `textSize` | `string` | ❌ | | +| `facets` | `ref[]` | ❌ | | + +--- + +### `pub.leaflet.blocks.unorderedList` + +#### Properties + +| Property | Type | Required | Description | +| ---------- | ------- | -------- | ----------- | +| `children` | `ref[]` | ✅ | | + +#### Defs + +##### `pub.leaflet.blocks.unorderedList#listItem` + +| Property | Type | Required | Description | +| --------------------- | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------- | +| `checked` | `boolean` | ❌ | If present, this item is a checklist item. true = checked, false = unchecked. If absent, this is a normal list item. | +| `content` | `union` | ✅ | | +| `children` | `ref[]` | ❌ | Nested unordered list items. Mutually exclusive with orderedListChildren; if both are present, children takes precedence. | +| `orderedListChildren` | `ref` | ❌ | Nested ordered list items. Mutually exclusive with children; if both are present, children takes precedence. | + +--- + +### `pub.leaflet.blocks.website` + +#### Properties + +| Property | Type | Required | Description | Comments | +| -------------- | -------- | -------- | ----------- | ----------------------------------- | +| `previewImage` | `blob` | ❌ | | maxSize: 1000000, accepts: image/\* | +| `title` | `string` | ❌ | | | +| `description` | `string` | ❌ | | | +| `src` | `string` | ✅ | | | + +--- + +### `pub.leaflet.pages.linearDocument` + +#### Properties + +| Property | Type | Required | Description | +| -------- | -------- | -------- | ----------- | +| `id` | `string` | ❌ | | +| `blocks` | `ref[]` | ✅ | | + +#### Defs + +##### `pub.leaflet.pages.linearDocument#block` + +| Property | Type | Required | Description | +| ----------- | -------- | -------- | ----------- | +| `block` | `union` | ✅ | | +| `alignment` | `string` | ❌ | | + +##### `pub.leaflet.pages.linearDocument#quote` + +| Property | Type | Required | Description | +| -------- | ----- | -------- | ----------- | +| `start` | `ref` | ✅ | | +| `end` | `ref` | ✅ | | + +##### `pub.leaflet.pages.linearDocument#position` + +| Property | Type | Required | Description | +| -------- | ----------- | -------- | ----------- | +| `block` | `integer[]` | ✅ | | +| `offset` | `integer` | ✅ | | + +--- + +### `pub.leaflet.richtext.facet` + +**Description:** Annotation of a sub-string within rich text. + +#### Properties + +| Property | Type | Required | Description | +| ---------- | --------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `index` | `ref` | ✅ | Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets. | +| `features` | `union[]` | ✅ | | + +#### Defs + +##### `pub.leaflet.richtext.facet#byteSlice` + +Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets. + +| Property | Type | Required | Description | +| ----------- | --------- | -------- | ----------- | +| `byteStart` | `integer` | ✅ | | +| `byteEnd` | `integer` | ✅ | | + +##### `pub.leaflet.richtext.facet#link` + +Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL. + +| Property | Type | Required | Description | +| -------- | -------- | -------- | ----------- | +| `uri` | `string` | ✅ | | + +##### `pub.leaflet.richtext.facet#didMention` + +Facet feature for mentioning a did. + +| Property | Type | Required | Description | +| -------- | -------- | -------- | ----------- | +| `did` | `string` | ✅ | | + +##### `pub.leaflet.richtext.facet#atMention` + +Facet feature for mentioning an AT URI. + +| Property | Type | Required | Description | +| -------- | -------- | -------- | ----------- | +| `atURI` | `string` | ✅ | | + +##### `pub.leaflet.richtext.facet#code` + +Facet feature for inline code. + +##### `pub.leaflet.richtext.facet#highlight` + +Facet feature for highlighted text. + +##### `pub.leaflet.richtext.facet#underline` + +Facet feature for underline markup + +##### `pub.leaflet.richtext.facet#strikethrough` + +Facet feature for strikethrough markup + +##### `pub.leaflet.richtext.facet#id` + +Facet feature for an identifier. Used for linking to a segment + +| Property | Type | Required | Description | +| -------- | -------- | -------- | ----------- | +| `id` | `string` | ❌ | | + +##### `pub.leaflet.richtext.facet#bold` + +Facet feature for bold text + +##### `pub.leaflet.richtext.facet#italic` + +Facet feature for italic text + +##### `pub.leaflet.richtext.facet#footnote` + +Facet feature for a footnote reference + +| Property | Type | Required | Description | +| ------------------ | -------- | -------- | ----------- | +| `footnoteId` | `string` | ✅ | | +| `contentPlaintext` | `string` | ✅ | | +| `contentFacets` | `ref[]` | ❌ | | + +--- + ## Notes - All timestamps use the `datetime` format (ISO 8601) diff --git a/docs/AUTO_GENERATION.md b/docs/AUTO_GENERATION.md index 7ecbf817..acfa6ed2 100644 --- a/docs/AUTO_GENERATION.md +++ b/docs/AUTO_GENERATION.md @@ -8,9 +8,8 @@ The `generated/exports.ts` file is **automatically generated** from lexicon JSON When you run `npm run gen-api`, the build process: -1. Runs `lex gen-api` to generate TypeScript types in `generated/` -2. Runs `scripts/create-shims.sh` to create type shims for external lexicons -3. Runs `scripts/generate-exports.js` to auto-generate `generated/exports.ts` +1. Runs `lex gen-api` to generate TypeScript types in `generated/` (including vendored external lexicons from `lexicons/pub/` and `lexicons/app/bsky/`) +2. Runs `scripts/generate-exports.js` to auto-generate `generated/exports.ts` ## The Generation Script diff --git a/lexicons/pub/leaflet/blocks/orderedList.json b/lexicons/pub/leaflet/blocks/orderedList.json index 3c1fe839..48bd35b0 100644 --- a/lexicons/pub/leaflet/blocks/orderedList.json +++ b/lexicons/pub/leaflet/blocks/orderedList.json @@ -23,6 +23,10 @@ "type": "object", "required": ["content"], "properties": { + "checked": { + "type": "boolean", + "description": "If present, this item is a checklist item. true = checked, false = unchecked. If absent, this is a normal list item." + }, "content": { "type": "union", "refs": [ diff --git a/lexicons/pub/leaflet/blocks/unorderedList.json b/lexicons/pub/leaflet/blocks/unorderedList.json index fdb408d3..9e03d7f5 100644 --- a/lexicons/pub/leaflet/blocks/unorderedList.json +++ b/lexicons/pub/leaflet/blocks/unorderedList.json @@ -19,6 +19,10 @@ "type": "object", "required": ["content"], "properties": { + "checked": { + "type": "boolean", + "description": "If present, this item is a checklist item. true = checked, false = unchecked. If absent, this is a normal list item." + }, "content": { "type": "union", "refs": [ diff --git a/lexicons/pub/leaflet/richtext/facet.json b/lexicons/pub/leaflet/richtext/facet.json index 7e6e12e8..62433b09 100644 --- a/lexicons/pub/leaflet/richtext/facet.json +++ b/lexicons/pub/leaflet/richtext/facet.json @@ -25,7 +25,8 @@ "#strikethrough", "#id", "#bold", - "#italic" + "#italic", + "#footnote" ] } } @@ -123,6 +124,26 @@ "description": "Facet feature for italic text", "required": [], "properties": {} + }, + "footnote": { + "type": "object", + "description": "Facet feature for a footnote reference", + "required": ["footnoteId", "contentPlaintext"], + "properties": { + "footnoteId": { + "type": "string" + }, + "contentPlaintext": { + "type": "string" + }, + "contentFacets": { + "type": "array", + "items": { + "type": "ref", + "ref": "#main" + } + } + } } } } diff --git a/package-lock.json b/package-lock.json index 7bbe2b2c..1cbf3660 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,6 @@ "version": "0.10.0", "license": "MIT", "dependencies": { - "@atcute/bluesky": "^3.2.15", - "@atcute/leaflet": "^1.0.15", "@atproto/lexicon": "^0.6.0", "multiformats": "^13.3.6" }, @@ -47,62 +45,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@atcute/atproto": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/@atcute/atproto/-/atproto-3.1.10.tgz", - "integrity": "sha512-+GKZpOc0PJcdWMQEkTfg/rSNDAAHxmAUGBl60g2az15etqJn5WaUPNGFE2sB7hKpwi5Ue2h/L0OacINcE/JDDQ==", - "license": "0BSD", - "dependencies": { - "@atcute/lexicons": "^1.2.6" - } - }, - "node_modules/@atcute/bluesky": { - "version": "3.2.15", - "resolved": "https://registry.npmjs.org/@atcute/bluesky/-/bluesky-3.2.15.tgz", - "integrity": "sha512-H4RW3WffjfdKvOZ9issEUQnuSR4KfuAwwJnYu0fclA9VDa99JTJ+pa8tTl9lFeBV9DINtWJAx7rdIbICoVCstQ==", - "license": "0BSD", - "dependencies": { - "@atcute/atproto": "^3.1.10", - "@atcute/lexicons": "^1.2.6" - } - }, - "node_modules/@atcute/leaflet": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/@atcute/leaflet/-/leaflet-1.0.15.tgz", - "integrity": "sha512-soA4jGsY0UxTar8rhtfQNYFXPI+qoQGgjpeMKJaO8ZCBBMRoo4OU9gsFJpUm/BYSocOBvvWSq2GV9pYEgUehTw==", - "license": "0BSD", - "dependencies": { - "@atcute/atproto": "^3.1.10", - "@atcute/lexicons": "^1.2.6" - } - }, - "node_modules/@atcute/lexicons": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@atcute/lexicons/-/lexicons-1.2.6.tgz", - "integrity": "sha512-s76UQd8D+XmHIzrjD9CJ9SOOeeLPHc+sMmcj7UFakAW/dDFXc579fcRdRfuUKvXBL5v1Gs2VgDdlh/IvvQZAwA==", - "license": "0BSD", - "dependencies": { - "@atcute/uint8array": "^1.0.6", - "@atcute/util-text": "^0.0.1", - "@standard-schema/spec": "^1.1.0", - "esm-env": "^1.2.2" - } - }, - "node_modules/@atcute/uint8array": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@atcute/uint8array/-/uint8array-1.0.6.tgz", - "integrity": "sha512-ucfRBQc7BFT8n9eCyGOzDHEMKF/nZwhS2pPao4Xtab1ML3HdFYcX2DM1tadCzas85QTGxHe5urnUAAcNKGRi9A==", - "license": "0BSD" - }, - "node_modules/@atcute/util-text": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@atcute/util-text/-/util-text-0.0.1.tgz", - "integrity": "sha512-t1KZqvn0AYy+h2KcJyHnKF9aEqfRfMUmyY8j1ELtAEIgqN9CxINAjxnoRCJIFUlvWzb+oY3uElQL/Vyk3yss0g==", - "license": "0BSD", - "dependencies": { - "unicode-segmenter": "^0.14.4" - } - }, "node_modules/@atproto/common-web": { "version": "0.4.9", "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.9.tgz", @@ -1906,6 +1848,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, "license": "MIT" }, "node_modules/@ts-morph/common": { @@ -2947,12 +2890,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/esm-env": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", - "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", - "license": "MIT" - }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", diff --git a/package.json b/package.json index 96e9fa99..2877813b 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "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", + "gen-api": "find ./lexicons -name '*.json' | xargs lex gen-api --yes ./generated && npm run gen-index", "gen-md": "find ./lexicons -name '*.json' | xargs lex gen-md --yes ./lexicons.md", "gen-schemas-md": "node ./scripts/generate-schemas.js", "//gen-ts": "UNUSED - use gen-api instead", @@ -65,8 +65,6 @@ "prepare": "husky && ./scripts/setup-merge-driver.sh" }, "dependencies": { - "@atcute/bluesky": "^3.2.15", - "@atcute/leaflet": "^1.0.15", "@atproto/lexicon": "^0.6.0", "multiformats": "^13.3.6" }, diff --git a/scripts/create-shims.sh b/scripts/create-shims.sh deleted file mode 100755 index 265666d7..00000000 --- a/scripts/create-shims.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/sh -# Create type shims for external lexicon references - -# @atcute/leaflet - Leaflet document types -mkdir -p ./generated/types/pub/leaflet/pages -cat >./generated/types/pub/leaflet/pages/linearDocument.ts <<'EOF' -// Type shim for @atcute/leaflet external lexicons -// This allows our generated code to resolve Leaflet type references -export * from '@atcute/leaflet/types/pages/linearDocument'; -EOF - -# app.bsky.richtext.facet - ATProto richtext facet types -mkdir -p ./generated/types/app/bsky/richtext -cat >./generated/types/app/bsky/richtext/facet.ts <<'EOF' -// Type shim for app.bsky.richtext.facet lexicon -// Re-exports from @atcute/bluesky which provides ATProto standard lexicon types -import type { AppBskyRichtextFacet } from '@atcute/bluesky'; -export type { AppBskyRichtextFacet }; -export type Main = AppBskyRichtextFacet.Main; -export type ByteSlice = AppBskyRichtextFacet.ByteSlice; -export type Link = AppBskyRichtextFacet.Link; -export type Mention = AppBskyRichtextFacet.Mention; -export type Tag = AppBskyRichtextFacet.Tag; -EOF - -echo "Created type shims for external lexicons" diff --git a/scripts/generate-exports.js b/scripts/generate-exports.js index 0f167a47..34498011 100644 --- a/scripts/generate-exports.js +++ b/scripts/generate-exports.js @@ -68,9 +68,24 @@ function pathToImportName(filePath) { return `${parentName.toUpperCase()}_DEFS_LEXICON_JSON`; } - // For external lexicons (com.atproto.*), use just the base name - if (filePath.startsWith("com/")) { - return `${splitCamelCase(baseName).toUpperCase()}_LEXICON_JSON`; + // For external lexicons (com.atproto.*, app.bsky.*, pub.leaflet.*), + // use enough path segments to avoid collisions between namespaces + if ( + filePath.startsWith("com/") || + filePath.startsWith("app/bsky/") || + filePath.startsWith("pub/") + ) { + const parts = parentDir.split("/"); + // Use last two meaningful path segments + baseName for disambiguation + // e.g., app/bsky/richtext/facet -> BSKY_RICHTEXT_FACET + // pub/leaflet/richtext/facet -> LEAFLET_RICHTEXT_FACET + // pub/leaflet/blocks/text -> LEAFLET_BLOCKS_TEXT + // com/atproto/repo/strongRef -> STRONG_REF (keep existing behavior) + if (filePath.startsWith("com/")) { + return `${splitCamelCase(baseName).toUpperCase()}_LEXICON_JSON`; + } + const prefix = parts.slice(1).join("_"); // skip top-level (app/pub/com) + return `${prefix.toUpperCase()}_${splitCamelCase(baseName).toUpperCase()}_LEXICON_JSON`; } // For files in subdirectories, include the subdirectory name diff --git a/tests/validate-external-lexicons.test.ts b/tests/validate-external-lexicons.test.ts new file mode 100644 index 00000000..897b7994 --- /dev/null +++ b/tests/validate-external-lexicons.test.ts @@ -0,0 +1,156 @@ +import { describe, it, expect } from "vitest"; +import { validate, ids } from "../generated/lexicons.js"; +import * as Activity from "../generated/types/org/hypercerts/claim/activity.js"; +import * as Collection from "../generated/types/org/hypercerts/collection.js"; +import * as Attachment from "../generated/types/org/hypercerts/context/attachment.js"; +import * as Measurement from "../generated/types/org/hypercerts/context/measurement.js"; + +/** + * Minimal valid Leaflet linearDocument (a single text block). + */ +const leafletDescription = { + $type: "pub.leaflet.pages.linearDocument" as const, + blocks: [ + { + $type: "pub.leaflet.pages.linearDocument#block" as const, + block: { + $type: "pub.leaflet.blocks.text" as const, + plaintext: "A rich-text description for testing.", + facets: [], + }, + }, + ], +}; + +/** + * Minimal valid Bluesky richtext facet (a link annotation). + */ +const bskyFacets = [ + { + index: { byteStart: 0, byteEnd: 4 }, + features: [ + { + $type: "app.bsky.richtext.facet#link" as const, + uri: "https://example.com", + }, + ], + }, +]; + +describe("external lexicon validation: pub.leaflet + app.bsky.richtext", () => { + describe("org.hypercerts.claim.activity", () => { + it("validates with a Leaflet description", () => { + const result = Activity.validateMain({ + $type: ids.OrgHypercertsClaimActivity, + title: "Reforestation 2024", + shortDescription: "Test activity", + createdAt: new Date().toISOString(), + description: leafletDescription, + }); + expect(result.success).toBe(true); + }); + + it("validates with Bluesky richtext facets", () => { + const result = Activity.validateMain({ + $type: ids.OrgHypercertsClaimActivity, + title: "Reforestation 2024", + shortDescription: "Link here and more", + createdAt: new Date().toISOString(), + shortDescriptionFacets: bskyFacets, + }); + expect(result.success).toBe(true); + }); + + it("validates with both description and facets", () => { + const result = Activity.validateMain({ + $type: ids.OrgHypercertsClaimActivity, + title: "Reforestation 2024", + shortDescription: "Link here and more", + createdAt: new Date().toISOString(), + description: leafletDescription, + shortDescriptionFacets: bskyFacets, + }); + expect(result.success).toBe(true); + }); + + it("rejects an invalid Leaflet description (missing blocks)", () => { + const result = validate( + { + $type: ids.OrgHypercertsClaimActivity, + title: "Test", + shortDescription: "Test", + createdAt: new Date().toISOString(), + description: { + $type: "pub.leaflet.pages.linearDocument", + // blocks is required but missing + }, + }, + ids.OrgHypercertsClaimActivity, + "main", + true, + ); + expect(result.success).toBe(false); + }); + }); + + describe("org.hypercerts.collection", () => { + it("validates with a Leaflet description", () => { + const result = Collection.validateMain({ + $type: ids.OrgHypercertsCollection, + title: "Test Collection", + createdAt: new Date().toISOString(), + description: leafletDescription, + }); + expect(result.success).toBe(true); + }); + + it("validates with Bluesky richtext facets", () => { + const result = Collection.validateMain({ + $type: ids.OrgHypercertsCollection, + title: "Test Collection", + shortDescription: "Link here", + createdAt: new Date().toISOString(), + shortDescriptionFacets: bskyFacets, + }); + expect(result.success).toBe(true); + }); + }); + + describe("org.hypercerts.context.attachment", () => { + it("validates with a Leaflet description", () => { + const result = Attachment.validateMain({ + $type: ids.OrgHypercertsContextAttachment, + title: "Evidence document", + createdAt: new Date().toISOString(), + description: leafletDescription, + }); + expect(result.success).toBe(true); + }); + + it("validates with Bluesky richtext facets", () => { + const result = Attachment.validateMain({ + $type: ids.OrgHypercertsContextAttachment, + title: "Evidence document", + shortDescription: "Link here", + createdAt: new Date().toISOString(), + shortDescriptionFacets: bskyFacets, + }); + expect(result.success).toBe(true); + }); + }); + + describe("org.hypercerts.context.measurement", () => { + it("validates with Bluesky richtext comment facets", () => { + const result = Measurement.validateMain({ + $type: ids.OrgHypercertsContextMeasurement, + metric: "carbon_sequestered", + unit: "tonnes_co2e", + value: "150.5", + createdAt: new Date().toISOString(), + comment: "Link here for details", + commentFacets: bskyFacets, + }); + expect(result.success).toBe(true); + }); + }); +}); From 137a04431f26d7cbe8a514f65a27bca0e6980c75 Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Sat, 4 Apr 2026 16:49:56 +0000 Subject: [PATCH 3/5] fix: wrap MIME types in backticks in SCHEMAS.md Fixes gen-schemas-md output to use backticks around MIME types in accept fields, improving readability and avoiding Prettier escaping `image/*` to `image/\*`. --- SCHEMAS.md | 142 ++++++++++++++++++------------------ scripts/generate-schemas.js | 7 +- 2 files changed, 72 insertions(+), 77 deletions(-) diff --git a/SCHEMAS.md b/SCHEMAS.md index 5876cb23..504f7a6d 100644 --- a/SCHEMAS.md +++ b/SCHEMAS.md @@ -129,18 +129,18 @@ A free-form string describing the work scope for simple or legacy scopes. #### Properties -| Property | Type | Required | Description | Comments | -| ------------------------ | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | -| `type` | `string` | ❌ | The type of this collection. Values beyond the known set are permitted. | maxLength: 64, Known values: `favorites`, `project`, `portfolio`, `program` | -| `title` | `string` | ✅ | Display name for this collection (e.g. 'Q1 2025 Impact Projects') | maxLength: 800, maxGraphemes: 80 | -| `shortDescription` | `string` | ❌ | Short summary of this collection, 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. | | -| `avatar` | `union` | ❌ | The collection's avatar/profile image as a URI or image blob. | | -| `banner` | `union` | ❌ | Larger horizontal image to display behind the collection view. | | -| `items` | `ref[]` | ❌ | Array of items in this collection with optional weights. | maxLength: 1000 | -| `location` | `ref` | ❌ | A strong reference to the location where this collection's activities were performed. The record referenced must conform with the lexicon app.certified.location. | | -| `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created | | +| Property | Type | Required | Description | Comments | +| ------------------------ | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | +| `type` | `string` | ❌ | The type of this collection. Values beyond the known set are permitted. | maxLength: 64 | +| `title` | `string` | ✅ | Display name for this collection (e.g. 'Q1 2025 Impact Projects') | maxLength: 800, maxGraphemes: 80 | +| `shortDescription` | `string` | ❌ | Short summary of this collection, 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. | | +| `avatar` | `union` | ❌ | The collection's avatar/profile image as a URI or image blob. | | +| `banner` | `union` | ❌ | Larger horizontal image to display behind the collection view. | | +| `items` | `ref[]` | ❌ | Array of items in this collection with optional weights. | maxLength: 1000 | +| `location` | `ref` | ❌ | A strong reference to the location where this collection's activities were performed. The record referenced must conform with the lexicon app.certified.location. | | +| `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created | | #### Defs @@ -181,17 +181,17 @@ An item in a collection, with an identifier and optional weight. #### Properties -| Property | Type | Required | Description | Comments | -| ------------------------ | --------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | -| `subjects` | `ref[]` | ❌ | References to the subject(s) the attachment is connected to—this may be an activity claim, outcome claim, measurement, evaluation, or even another attachment. This is optional as the attachment can exist before the claim is recorded. | maxLength: 100 | -| `contentType` | `string` | ❌ | The type of attachment. Values beyond the known set are permitted. | maxLength: 64, Known values: `report`, `audit`, `evidence`, `testimonial`, `methodology` | -| `content` | `union[]` | ❌ | The files, documents, or external references included in this attachment record. | maxLength: 100 | -| `title` | `string` | ✅ | Display title for this attachment (e.g. 'Impact Assessment Report', 'Audit Findings') | maxLength: 256 | -| `shortDescription` | `string` | ❌ | Short summary of this attachment, 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. | | -| `location` | `ref` | ❌ | A strong reference to the location where this attachment's subject matter occurred. The record referenced must conform with the lexicon app.certified.location. | | -| `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created. | | +| Property | Type | Required | Description | Comments | +| ------------------------ | --------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | +| `subjects` | `ref[]` | ❌ | References to the subject(s) the attachment is connected to—this may be an activity claim, outcome claim, measurement, evaluation, or even another attachment. This is optional as the attachment can exist before the claim is recorded. | maxLength: 100 | +| `contentType` | `string` | ❌ | The type of attachment. Values beyond the known set are permitted. | maxLength: 64 | +| `content` | `union[]` | ❌ | The files, documents, or external references included in this attachment record. | maxLength: 100 | +| `title` | `string` | ✅ | Display title for this attachment (e.g. 'Impact Assessment Report', 'Audit Findings') | maxLength: 256 | +| `shortDescription` | `string` | ❌ | Short summary of this attachment, 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. | | +| `location` | `ref` | ❌ | A strong reference to the location where this attachment's subject matter occurred. The record referenced must conform with the lexicon app.certified.location. | | +| `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created. | | --- @@ -289,7 +289,7 @@ Overall score for an evaluation on a numeric scale. | ------------ | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | | `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` | +| `version` | `string` | ✅ | CEL context schema version. | maxLength: 16 | | `createdAt` | `string` | ✅ | Client-declared timestamp when this expression was originally created. | | --- @@ -302,19 +302,19 @@ Overall score for an evaluation on a numeric scale. #### Properties -| 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. | | +| 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 | +| `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 | +| `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. | | --- @@ -330,15 +330,15 @@ Certified lexicons are common/shared lexicons that can be used across multiple p #### Properties -| Property | Type | Required | Description | Comments | -| -------------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| `lpVersion` | `string` | ✅ | The version of the Location Protocol | maxLength: 10 | -| `srs` | `string` | ✅ | The Spatial Reference System URI (e.g., http://www.opengis.net/def/crs/OGC/1.3/CRS84) that defines the coordinate system. | maxLength: 100 | -| `locationType` | `string` | ✅ | An identifier for the format of the location data (e.g., coordinate-decimal, geojson-point). See the Location Protocol spec for the full registry: https://spec.decentralizedgeo.org/specification/location-types/#location-type-registry | maxLength: 20, Known values: `coordinate-decimal`, `geojson-point`, `geojson`, `h3`, `geohash`, `wkt`, `address`, `scaledCoordinates` | -| `location` | `union` | ✅ | The location of where the work was performed as a URI, blob, or inline string. | | -| `name` | `string` | ❌ | Human-readable name for this location (e.g. 'Golden Gate Park', 'San Francisco Bay Area') | maxLength: 1000, maxGraphemes: 100 | -| `description` | `string` | ❌ | Additional context about this location, such as its significance to the work or specific boundaries | maxLength: 2000, maxGraphemes: 500 | -| `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created | | +| Property | Type | Required | Description | Comments | +| -------------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | +| `lpVersion` | `string` | ✅ | The version of the Location Protocol | maxLength: 10 | +| `srs` | `string` | ✅ | The Spatial Reference System URI (e.g., http://www.opengis.net/def/crs/OGC/1.3/CRS84) that defines the coordinate system. | maxLength: 100 | +| `locationType` | `string` | ✅ | An identifier for the format of the location data (e.g., coordinate-decimal, geojson-point). See the Location Protocol spec for the full registry: https://spec.decentralizedgeo.org/specification/location-types/#location-type-registry | maxLength: 20 | +| `location` | `union` | ✅ | The location of where the work was performed as a URI, blob, or inline string. | | +| `name` | `string` | ❌ | Human-readable name for this location (e.g. 'Golden Gate Park', 'San Francisco Bay Area') | maxLength: 1000, maxGraphemes: 100 | +| `description` | `string` | ❌ | Additional context about this location, such as its significance to the work or specific boundaries | maxLength: 2000, maxGraphemes: 500 | +| `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created | | #### Defs @@ -360,14 +360,14 @@ A location represented as a string, e.g. coordinates or a small GeoJSON string. #### Properties -| Property | Type | Required | Description | Comments | -| ---------------- | -------- | -------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | -| `badgeType` | `string` | ✅ | Category of the badge. Values beyond the known set are permitted. | maxLength: 100, Known values: `endorsement`, `verification`, `participation`, `certification`, `affiliation`, `recognition` | -| `title` | `string` | ✅ | Human-readable title of the badge. | maxLength: 256 | -| `icon` | `blob` | ❌ | Icon representing the badge, stored as a blob for compact visual display. | maxSize: 1048576, accepts: image/png, image/jpeg, image/webp, image/svg+xml | -| `description` | `string` | ❌ | Optional short statement describing what the badge represents. | maxLength: 5000, maxGraphemes: 500 | -| `allowedIssuers` | `ref[]` | ❌ | Optional allowlist of DIDs allowed to issue this badge. If omitted, anyone may issue it. | maxLength: 100 | -| `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created | | +| Property | Type | Required | Description | Comments | +| ---------------- | -------- | -------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | +| `badgeType` | `string` | ✅ | Category of the badge. Values beyond the known set are permitted. | maxLength: 100 | +| `title` | `string` | ✅ | Human-readable title of the badge. | maxLength: 256 | +| `icon` | `blob` | ❌ | Icon representing the badge, stored as a blob for compact visual display. | maxSize: 1048576, accepts: `image/png`, `image/jpeg`, `image/webp`, `image/svg+xml` | +| `description` | `string` | ❌ | Optional short statement describing what the badge represents. | maxLength: 5000, maxGraphemes: 500 | +| `allowedIssuers` | `ref[]` | ❌ | Optional allowlist of DIDs allowed to issue this badge. If omitted, anyone may issue it. | maxLength: 100 | +| `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created | | --- @@ -397,12 +397,12 @@ A location represented as a string, e.g. coordinates or a small GeoJSON string. #### Properties -| Property | Type | Required | Description | Comments | -| ------------ | -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | -| `badgeAward` | `ref` | ✅ | Strong reference to the badge award being responded to. The record referenced must conform with the lexicon app.certified.badge.award. | | -| `response` | `string` | ✅ | The recipient’s response for the badge (accepted or rejected). | Known values: `accepted`, `rejected` | -| `weight` | `string` | ❌ | Optional relative weight for accepted badges, assigned by the recipient. | maxLength: 50 | -| `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created | | +| Property | Type | Required | Description | Comments | +| ------------ | -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| `badgeAward` | `ref` | ✅ | Strong reference to the badge award being responded to. The record referenced must conform with the lexicon app.certified.badge.award. | | +| `response` | `string` | ✅ | The recipient’s response for the badge (accepted or rejected). | | +| `weight` | `string` | ❌ | Optional relative weight for accepted badges, assigned by the recipient. | maxLength: 50 | +| `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created | | --- @@ -782,11 +782,11 @@ Configuration for a specific contributor within a board. Values serve as fallbac #### Properties -| Property | Type | Required | Description | Comments | -| ------------- | -------- | -------- | ----------------------------------------------------- | ----------------------------------- | -| `image` | `blob` | ✅ | | maxSize: 1000000, accepts: image/\* | -| `alt` | `string` | ❌ | Alt text description of the image, for accessibility. | | -| `aspectRatio` | `ref` | ✅ | | | +| Property | Type | Required | Description | Comments | +| ------------- | -------- | -------- | ----------------------------------------------------- | ------------------------------------ | +| `image` | `blob` | ✅ | | maxSize: 1000000, accepts: `image/*` | +| `alt` | `string` | ❌ | Alt text description of the image, for accessibility. | | +| `aspectRatio` | `ref` | ✅ | | | #### Defs @@ -888,12 +888,12 @@ Configuration for a specific contributor within a board. Values serve as fallbac #### Properties -| Property | Type | Required | Description | Comments | -| -------------- | -------- | -------- | ----------- | ----------------------------------- | -| `previewImage` | `blob` | ❌ | | maxSize: 1000000, accepts: image/\* | -| `title` | `string` | ❌ | | | -| `description` | `string` | ❌ | | | -| `src` | `string` | ✅ | | | +| Property | Type | Required | Description | Comments | +| -------------- | -------- | -------- | ----------- | ------------------------------------ | +| `previewImage` | `blob` | ❌ | | maxSize: 1000000, accepts: `image/*` | +| `title` | `string` | ❌ | | | +| `description` | `string` | ❌ | | | +| `src` | `string` | ✅ | | | --- diff --git a/scripts/generate-schemas.js b/scripts/generate-schemas.js index 1fb7e4d1..faa5a6f1 100644 --- a/scripts/generate-schemas.js +++ b/scripts/generate-schemas.js @@ -53,12 +53,7 @@ function getComments(prop) { if (prop.maxGraphemes) comments.push(`maxGraphemes: ${prop.maxGraphemes}`); if (prop.maxSize) comments.push(`maxSize: ${prop.maxSize}`); if (prop.accept?.length > 0) - comments.push(`accepts: ${prop.accept.join(", ")}`); - if (prop.knownValues) { - comments.push( - `Known values: ${prop.knownValues.map((v) => `\`${v}\``).join(", ")}`, - ); - } + comments.push(`accepts: ${prop.accept.map((a) => `\`${a}\``).join(", ")}`); return comments.join(", "); } From dd532d91eb6cf2d093b1791659f521d0a905172d Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Sat, 4 Apr 2026 16:54:13 +0000 Subject: [PATCH 4/5] test: exercise pub.leaflet.richtext.facet in test fixture Add a bold facet to the leafletDescription fixture so the test exercises the pub.leaflet.richtext.facet lexicon at runtime, not just an empty array. --- tests/validate-external-lexicons.test.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/validate-external-lexicons.test.ts b/tests/validate-external-lexicons.test.ts index 897b7994..9ce32d1e 100644 --- a/tests/validate-external-lexicons.test.ts +++ b/tests/validate-external-lexicons.test.ts @@ -6,7 +6,16 @@ import * as Attachment from "../generated/types/org/hypercerts/context/attachmen import * as Measurement from "../generated/types/org/hypercerts/context/measurement.js"; /** - * Minimal valid Leaflet linearDocument (a single text block). + * Minimal valid Leaflet richtext facet (bold annotation). + * Exercises pub.leaflet.richtext.facet at runtime. + */ +const leafletFacet = { + index: { byteStart: 0, byteEnd: 9 }, + features: [{ $type: "pub.leaflet.richtext.facet#bold" as const }], +}; + +/** + * Minimal valid Leaflet linearDocument (a single text block with a facet). */ const leafletDescription = { $type: "pub.leaflet.pages.linearDocument" as const, @@ -16,7 +25,7 @@ const leafletDescription = { block: { $type: "pub.leaflet.blocks.text" as const, plaintext: "A rich-text description for testing.", - facets: [], + facets: [leafletFacet], }, }, ], From 238f8a108f0455aa6f9c8b719ac3748e18eb47d5 Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Sat, 4 Apr 2026 17:00:04 +0000 Subject: [PATCH 5/5] fix: avoid double blank line for empty property tables in SCHEMAS.md When a lexicon has properties: {} (e.g. horizontalRule), the gen script emitted a trailing blank line after the Properties heading plus another from the section separator, producing a double blank line that Prettier collapsed but CI's raw gen output preserved. Also wraps MIME types in accept fields with backticks. --- SCHEMAS.md | 120 ++++++++++++++++++------------------ scripts/generate-schemas.js | 15 ++++- 2 files changed, 72 insertions(+), 63 deletions(-) diff --git a/SCHEMAS.md b/SCHEMAS.md index 504f7a6d..4cce8ea8 100644 --- a/SCHEMAS.md +++ b/SCHEMAS.md @@ -129,18 +129,18 @@ A free-form string describing the work scope for simple or legacy scopes. #### Properties -| Property | Type | Required | Description | Comments | -| ------------------------ | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | -| `type` | `string` | ❌ | The type of this collection. Values beyond the known set are permitted. | maxLength: 64 | -| `title` | `string` | ✅ | Display name for this collection (e.g. 'Q1 2025 Impact Projects') | maxLength: 800, maxGraphemes: 80 | -| `shortDescription` | `string` | ❌ | Short summary of this collection, 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. | | -| `avatar` | `union` | ❌ | The collection's avatar/profile image as a URI or image blob. | | -| `banner` | `union` | ❌ | Larger horizontal image to display behind the collection view. | | -| `items` | `ref[]` | ❌ | Array of items in this collection with optional weights. | maxLength: 1000 | -| `location` | `ref` | ❌ | A strong reference to the location where this collection's activities were performed. The record referenced must conform with the lexicon app.certified.location. | | -| `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created | | +| Property | Type | Required | Description | Comments | +| ------------------------ | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| `type` | `string` | ❌ | The type of this collection. Values beyond the known set are permitted. | maxLength: 64, Known values: `favorites`, `project`, `portfolio`, `program` | +| `title` | `string` | ✅ | Display name for this collection (e.g. 'Q1 2025 Impact Projects') | maxLength: 800, maxGraphemes: 80 | +| `shortDescription` | `string` | ❌ | Short summary of this collection, 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. | | +| `avatar` | `union` | ❌ | The collection's avatar/profile image as a URI or image blob. | | +| `banner` | `union` | ❌ | Larger horizontal image to display behind the collection view. | | +| `items` | `ref[]` | ❌ | Array of items in this collection with optional weights. | maxLength: 1000 | +| `location` | `ref` | ❌ | A strong reference to the location where this collection's activities were performed. The record referenced must conform with the lexicon app.certified.location. | | +| `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created | | #### Defs @@ -181,17 +181,17 @@ An item in a collection, with an identifier and optional weight. #### Properties -| Property | Type | Required | Description | Comments | -| ------------------------ | --------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | -| `subjects` | `ref[]` | ❌ | References to the subject(s) the attachment is connected to—this may be an activity claim, outcome claim, measurement, evaluation, or even another attachment. This is optional as the attachment can exist before the claim is recorded. | maxLength: 100 | -| `contentType` | `string` | ❌ | The type of attachment. Values beyond the known set are permitted. | maxLength: 64 | -| `content` | `union[]` | ❌ | The files, documents, or external references included in this attachment record. | maxLength: 100 | -| `title` | `string` | ✅ | Display title for this attachment (e.g. 'Impact Assessment Report', 'Audit Findings') | maxLength: 256 | -| `shortDescription` | `string` | ❌ | Short summary of this attachment, 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. | | -| `location` | `ref` | ❌ | A strong reference to the location where this attachment's subject matter occurred. The record referenced must conform with the lexicon app.certified.location. | | -| `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created. | | +| Property | Type | Required | Description | Comments | +| ------------------------ | --------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| `subjects` | `ref[]` | ❌ | References to the subject(s) the attachment is connected to—this may be an activity claim, outcome claim, measurement, evaluation, or even another attachment. This is optional as the attachment can exist before the claim is recorded. | maxLength: 100 | +| `contentType` | `string` | ❌ | The type of attachment. Values beyond the known set are permitted. | maxLength: 64, Known values: `report`, `audit`, `evidence`, `testimonial`, `methodology` | +| `content` | `union[]` | ❌ | The files, documents, or external references included in this attachment record. | maxLength: 100 | +| `title` | `string` | ✅ | Display title for this attachment (e.g. 'Impact Assessment Report', 'Audit Findings') | maxLength: 256 | +| `shortDescription` | `string` | ❌ | Short summary of this attachment, 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. | | +| `location` | `ref` | ❌ | A strong reference to the location where this attachment's subject matter occurred. The record referenced must conform with the lexicon app.certified.location. | | +| `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created. | | --- @@ -289,7 +289,7 @@ Overall score for an evaluation on a numeric scale. | ------------ | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | | `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 | +| `version` | `string` | ✅ | CEL context schema version. | maxLength: 16, Known values: `v1` | | `createdAt` | `string` | ✅ | Client-declared timestamp when this expression was originally created. | | --- @@ -302,19 +302,19 @@ Overall score for an evaluation on a numeric scale. #### Properties -| 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 | -| `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 | -| `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. | | +| 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. | | --- @@ -330,15 +330,15 @@ Certified lexicons are common/shared lexicons that can be used across multiple p #### Properties -| Property | Type | Required | Description | Comments | -| -------------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | -| `lpVersion` | `string` | ✅ | The version of the Location Protocol | maxLength: 10 | -| `srs` | `string` | ✅ | The Spatial Reference System URI (e.g., http://www.opengis.net/def/crs/OGC/1.3/CRS84) that defines the coordinate system. | maxLength: 100 | -| `locationType` | `string` | ✅ | An identifier for the format of the location data (e.g., coordinate-decimal, geojson-point). See the Location Protocol spec for the full registry: https://spec.decentralizedgeo.org/specification/location-types/#location-type-registry | maxLength: 20 | -| `location` | `union` | ✅ | The location of where the work was performed as a URI, blob, or inline string. | | -| `name` | `string` | ❌ | Human-readable name for this location (e.g. 'Golden Gate Park', 'San Francisco Bay Area') | maxLength: 1000, maxGraphemes: 100 | -| `description` | `string` | ❌ | Additional context about this location, such as its significance to the work or specific boundaries | maxLength: 2000, maxGraphemes: 500 | -| `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created | | +| Property | Type | Required | Description | Comments | +| -------------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `lpVersion` | `string` | ✅ | The version of the Location Protocol | maxLength: 10 | +| `srs` | `string` | ✅ | The Spatial Reference System URI (e.g., http://www.opengis.net/def/crs/OGC/1.3/CRS84) that defines the coordinate system. | maxLength: 100 | +| `locationType` | `string` | ✅ | An identifier for the format of the location data (e.g., coordinate-decimal, geojson-point). See the Location Protocol spec for the full registry: https://spec.decentralizedgeo.org/specification/location-types/#location-type-registry | maxLength: 20, Known values: `coordinate-decimal`, `geojson-point`, `geojson`, `h3`, `geohash`, `wkt`, `address`, `scaledCoordinates` | +| `location` | `union` | ✅ | The location of where the work was performed as a URI, blob, or inline string. | | +| `name` | `string` | ❌ | Human-readable name for this location (e.g. 'Golden Gate Park', 'San Francisco Bay Area') | maxLength: 1000, maxGraphemes: 100 | +| `description` | `string` | ❌ | Additional context about this location, such as its significance to the work or specific boundaries | maxLength: 2000, maxGraphemes: 500 | +| `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created | | #### Defs @@ -360,14 +360,14 @@ A location represented as a string, e.g. coordinates or a small GeoJSON string. #### Properties -| Property | Type | Required | Description | Comments | -| ---------------- | -------- | -------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | -| `badgeType` | `string` | ✅ | Category of the badge. Values beyond the known set are permitted. | maxLength: 100 | -| `title` | `string` | ✅ | Human-readable title of the badge. | maxLength: 256 | -| `icon` | `blob` | ❌ | Icon representing the badge, stored as a blob for compact visual display. | maxSize: 1048576, accepts: `image/png`, `image/jpeg`, `image/webp`, `image/svg+xml` | -| `description` | `string` | ❌ | Optional short statement describing what the badge represents. | maxLength: 5000, maxGraphemes: 500 | -| `allowedIssuers` | `ref[]` | ❌ | Optional allowlist of DIDs allowed to issue this badge. If omitted, anyone may issue it. | maxLength: 100 | -| `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created | | +| Property | Type | Required | Description | Comments | +| ---------------- | -------- | -------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| `badgeType` | `string` | ✅ | Category of the badge. Values beyond the known set are permitted. | maxLength: 100, Known values: `endorsement`, `verification`, `participation`, `certification`, `affiliation`, `recognition` | +| `title` | `string` | ✅ | Human-readable title of the badge. | maxLength: 256 | +| `icon` | `blob` | ❌ | Icon representing the badge, stored as a blob for compact visual display. | maxSize: 1048576, accepts: `image/png`, `image/jpeg`, `image/webp`, `image/svg+xml` | +| `description` | `string` | ❌ | Optional short statement describing what the badge represents. | maxLength: 5000, maxGraphemes: 500 | +| `allowedIssuers` | `ref[]` | ❌ | Optional allowlist of DIDs allowed to issue this badge. If omitted, anyone may issue it. | maxLength: 100 | +| `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created | | --- @@ -397,12 +397,12 @@ A location represented as a string, e.g. coordinates or a small GeoJSON string. #### Properties -| Property | Type | Required | Description | Comments | -| ------------ | -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -| `badgeAward` | `ref` | ✅ | Strong reference to the badge award being responded to. The record referenced must conform with the lexicon app.certified.badge.award. | | -| `response` | `string` | ✅ | The recipient’s response for the badge (accepted or rejected). | | -| `weight` | `string` | ❌ | Optional relative weight for accepted badges, assigned by the recipient. | maxLength: 50 | -| `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created | | +| Property | Type | Required | Description | Comments | +| ------------ | -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | +| `badgeAward` | `ref` | ✅ | Strong reference to the badge award being responded to. The record referenced must conform with the lexicon app.certified.badge.award. | | +| `response` | `string` | ✅ | The recipient’s response for the badge (accepted or rejected). | Known values: `accepted`, `rejected` | +| `weight` | `string` | ❌ | Optional relative weight for accepted badges, assigned by the recipient. | maxLength: 50 | +| `createdAt` | `string` | ✅ | Client-declared timestamp when this record was originally created | | --- diff --git a/scripts/generate-schemas.js b/scripts/generate-schemas.js index faa5a6f1..3598ea7e 100644 --- a/scripts/generate-schemas.js +++ b/scripts/generate-schemas.js @@ -54,6 +54,11 @@ function getComments(prop) { if (prop.maxSize) comments.push(`maxSize: ${prop.maxSize}`); if (prop.accept?.length > 0) comments.push(`accepts: ${prop.accept.map((a) => `\`${a}\``).join(", ")}`); + if (prop.knownValues) { + comments.push( + `Known values: ${prop.knownValues.map((v) => `\`${v}\``).join(", ")}`, + ); + } return comments.join(", "); } @@ -247,18 +252,20 @@ function generateMainSection(mainDef, lexicon) { const propsSource = mainDef.record || mainDef; const hasProperties = propsSource.properties !== undefined; + let hasPropertyRows = false; if (hasProperties) { - output.push("#### Properties", ""); const required = propsSource.required || []; const rows = extractPropertyRows(propsSource, required, lexicon.data.defs); + output.push("#### Properties", ""); if (rows.length > 0) { + hasPropertyRows = true; const hasComments = rows.some((r) => r.comments); output.push(...renderTable(rows, hasComments)); } } - return { output, hasProperties }; + return { output, hasProperties, hasPropertyRows }; } function generateAdditionalDefsSection(lexicon, hasPropertiesBefore = false) { @@ -308,10 +315,12 @@ function generateLexiconSection(lexicon, isFirst = false) { // Use lexicon description if no main, otherwise use main's description let hasProperties = false; + let hasPropertyRows = false; if (mainDef) { const mainResult = generateMainSection(mainDef, lexicon); output.push(...mainResult.output); hasProperties = mainResult.hasProperties; + hasPropertyRows = mainResult.hasPropertyRows; } else { output.push(...generateDescription(lexicon.data.description)); } @@ -322,7 +331,7 @@ function generateLexiconSection(lexicon, isFirst = false) { // Add trailing blank unless we end with just description/key (no tables) const hasAdditionalDefs = additionalDefs.length > 0; - if (hasProperties || hasAdditionalDefs) { + if (hasPropertyRows || hasAdditionalDefs) { output.push(""); }