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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/clarify-knownvalues-open-vocabulary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hypercerts-org/lexicon": patch
---

Clarify that `knownValues` is an open vocabulary, not a closed enum. Adds a "Schema Conventions" section to README.md explaining that custom string values are permitted on any `knownValues` field (and contrasting with `enum`, which is closed and validator-enforced). Updates inline descriptions on `app.certified.location#locationType`, `org.hypercerts.workscope.tag#category`, and `org.hypercerts.workscope.tag#status` to explicitly note that values beyond the listed set are permitted — bringing them in line with the existing wording on `org.hypercerts.collection#type`, `org.hypercerts.context.attachment#contentType`, and `app.certified.badge.definition#badgeType`. The `locationType` description now also explicitly calls out that polygons / multipolygons / featurecollections use the catch-all `geojson` entry rather than a typed variant. Fixes a misleading line in STRING_CONSTRAINTS.md that conflated `knownValues` with `enum`. Documentation-only — no schema or type changes.
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,58 @@ await agent.api.com.atproto.repo.createRecord({

> **Full property tables** → [SCHEMAS.md](SCHEMAS.md)

## Schema Conventions

A few conventions are worth knowing before you start emitting records,
because they affect what counts as a "valid" record vs. one that breaks
interop with the wider ecosystem.

### `knownValues` is an open vocabulary, not a closed enum

When you see `"knownValues": [...]` on a string field, those values are
**conventions for interoperability, not constraints.** Lexicon validators
do not reject values outside the list. The vocabulary is intentionally
open so applications can extend it with domain-specific variants without
waiting for a schema bump.

If the lexicon authors wanted a field to be closed, they would have used
`enum` instead — which the validator does enforce.

In practice:

- **Prefer a `knownValues` entry** when one matches your semantics. It
guarantees other Hypercerts-aware consumers (indexers, AppViews,
search facets, dropdown UIs) will recognize the value without
per-app special-casing.
- **Custom values are permitted** when no listed value fits. The record
is still wire-valid. Consumers that filter on the canonical list
simply won't categorize the value; consumers that read the raw string
will see whatever you wrote.
- **Custom values are at-your-own-risk for interop.** A custom
`locationType: "geojson-polygon"`, for example, is a perfectly valid
record — but a downstream tool that buckets by the Location Protocol
registry will treat it as "other". Use the closest `knownValues`
entry (here, `geojson`, which is the catch-all for non-Point GeoJSON)
when you want canonical bucketing.

When a `knownValues` list grows in a way that's broadly useful, open a
PR to add the new entry to the lexicon — that's how the canonical list
stays in sync with real-world usage.

### `strongRef` pins to a specific record version

Fields typed as `com.atproto.repo.strongRef` carry both `uri` AND `cid`.
The `cid` is a content hash that pins the reference to the exact record
version at the time the reference was written — if the referenced
record is later overwritten, consumers can detect that the reference
has drifted.

This matters when you might otherwise be tempted to embed a full record
by value to "snapshot" it. A strongRef + CID already provides the
snapshot semantic, and it's what the lexicon uses across
`badge.award.badge`, `badge.response.badgeAward`, `funding.receipt.for`,
and similar fields where the historical content needs to stay stable.

## Entity Relationship Diagram

![Hypercert ERD](ERD.svg)
Expand Down
22 changes: 11 additions & 11 deletions SCHEMAS.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,10 +316,10 @@ A free-text string value (e.g. a display name, wallet address, or other identifi
| ------------------- | ---------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| `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` |
| `category` | `string` | ❌ | Category type of this scope. Values beyond the known set are permitted. | 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` |
| `status` | `string` | ❌ | Lifecycle status of this tag. Communities propose tags, curators accept them, deprecated tags point to replacements via supersededBy. Values beyond the known set are permitted. | 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 |
Expand All @@ -340,15 +340,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. Use `geojson-point` for a single GeoJSON Point; use `geojson` as the catch-all for any other GeoJSON geometry (Polygon, MultiPolygon, FeatureCollection, etc.) — the inner payload's own GeoJSON `type` field carries the specifics. Values beyond the known set are permitted; see the Location Protocol spec for the canonical 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

Expand Down
3 changes: 2 additions & 1 deletion STRING_CONSTRAINTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ When both are used, the style guide recommends a **10–20:1 ratio** for `maxLen
### When to use neither

- **Format-typed fields** (`datetime`, `at-uri`, `uri`, `did`, `cid`, etc.) do **not** get `maxLength`/`maxGraphemes` — the format itself constrains the value. The style guide explicitly says: _"Don't redundantly specify both a format and length limits."_
- **`knownValues`/`enum` fields** do not need length constraints; the allowed values are already defined.
- **`enum` fields** do not need length constraints — the allowed values are fully defined and the validator enforces them.
- **`knownValues` fields** are an open vocabulary (any string is permitted; the listed values are interoperability conventions). They still need `maxLength` to bound the size of custom values, but `maxGraphemes` is usually unnecessary because the known values themselves are short.
- **Array `maxLength`** limits the number of items, not byte size. Used to prevent unbounded arrays.

---
Expand Down
2 changes: 1 addition & 1 deletion lexicons/app/certified/location.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
},
"locationType": {
"type": "string",
"description": "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",
"description": "An identifier for the format of the location data. Use `geojson-point` for a single GeoJSON Point; use `geojson` as the catch-all for any other GeoJSON geometry (Polygon, MultiPolygon, FeatureCollection, etc.) — the inner payload's own GeoJSON `type` field carries the specifics. Values beyond the known set are permitted; see the Location Protocol spec for the canonical registry: https://spec.decentralizedgeo.org/specification/location-types/#location-type-registry",
"knownValues": [
"coordinate-decimal",
"geojson-point",
Expand Down
4 changes: 2 additions & 2 deletions lexicons/org/hypercerts/workscope/tag.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
},
"category": {
"type": "string",
"description": "Category type of this scope.",
"description": "Category type of this scope. Values beyond the known set are permitted.",
"knownValues": ["topic", "language", "domain", "method"],
"maxLength": 50
},
Expand All @@ -39,7 +39,7 @@
},
"status": {
"type": "string",
"description": "Lifecycle status of this tag. Communities propose tags, curators accept them, deprecated tags point to replacements via supersededBy.",
"description": "Lifecycle status of this tag. Communities propose tags, curators accept them, deprecated tags point to replacements via supersededBy. Values beyond the known set are permitted.",
"knownValues": ["proposed", "accepted", "deprecated"],
"maxLength": 20
},
Expand Down