))}
diff --git a/standard/schema/mapping-assertion.ts b/standard/schema/mapping-assertion.ts
index be7a7a1..aebb7be 100644
--- a/standard/schema/mapping-assertion.ts
+++ b/standard/schema/mapping-assertion.ts
@@ -20,8 +20,8 @@ export const MappingAssertionBase = AdminMetadata.extend({
subject: WorkIri,
relation: z.enum(['exactMatch', 'closeMatch']),
target: z.object({
- target_kind: z.string().optional(),
identifier: Iri,
+ conforms_to: z.union([Iri, z.array(Iri).min(1)]).optional(),
}),
source: z.string().min(1),
});
From e54b883cd9ad9b561a24e9adb824f8e83ae125d7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
<14755525+maehr@users.noreply.github.com>
Date: Tue, 9 Jun 2026 23:39:32 +0200
Subject: [PATCH 10/24] chore(data): bump submodule to registry main (36cae56)
textrefs/registry#1 merged via squash; advance data/ pointer from the
feature branch tip to the merge commit on main so the Validate
workflow's "pin is on registry main" guard passes.
Co-Authored-By: Claude Opus 4.7
---
data | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/data b/data
index be413a0..36cae56 160000
--- a/data
+++ b/data
@@ -1 +1 @@
-Subproject commit be413a092bd71b36fecc04a91ac3c0b052f9c097
+Subproject commit 36cae56593807e6e5ebbdd4eb96878eb74229f9d
From 666dc78138b52def591ec15fcac1e52d73f9f3eb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
<14755525+maehr@users.noreply.github.com>
Date: Wed, 10 Jun 2026 00:00:09 +0200
Subject: [PATCH 11/24] fix(404): mark docs/404.mdx as draft to drop catch-all
route conflict
Starlight reads docs/404 via getEntry() for its dedicated /404 route
AND enumerates the same entry through the [...slug] catch-all,
producing a benign but noisy build warning ("Could not render /404
from route /[...slug] as it conflicts with higher priority route
/404"). draft: true excludes the entry from the catch-all in
production builds while leaving Starlight's direct-by-id lookup
intact, so dist/404.html still ships our custom hero.
The localised dist/de/404/index.html is dropped (the fallback-route
pass uses the same draft-filtered docs list); Cloudflare Pages serves
/404.html for missing pages across all locales anyway.
Co-Authored-By: Claude Opus 4.7
---
src/content/docs/404.mdx | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/content/docs/404.mdx b/src/content/docs/404.mdx
index 2f38e0b..8dba66b 100644
--- a/src/content/docs/404.mdx
+++ b/src/content/docs/404.mdx
@@ -3,6 +3,7 @@ title: '404'
description: This page could not be found. Help improve TextRefs by reporting broken links or missing content.
template: splash
editUrl: false
+draft: true
hero:
title: This reference is missing.
tagline: If you expected a TextRefs page here, please help us improve the registry and documentation.
From 24b2e362c56734e0e6609268383c9af3865ce2b6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
Date: Fri, 26 Jun 2026 10:19:09 +0200
Subject: [PATCH 12/24] docs: add ORCID for Luz Christopher Seiberth (#19)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* chore(profile): bump github-profile submodule
Refresh org profile README and add a top-level README in
textrefs/.github.
Co-Authored-By: Claude Opus 4.8
* docs: add ORCID for Luz Christopher Seiberth
Record ORCID 0000-0002-5606-0964 for the second author/creator in
CITATION.cff and the project's Zenodo metadata, matching the existing
ORCID entry for the first author. Checksum-verified valid.
Co-Authored-By: Claude Opus 4.8
---------
Co-authored-by: Moritz Mähr <14755525+maehr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8
---
.zenodo.json | 3 ++-
CITATION.cff | 2 +-
github-profile | 2 +-
3 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/.zenodo.json b/.zenodo.json
index e3da71a..b9f58dd 100644
--- a/.zenodo.json
+++ b/.zenodo.json
@@ -17,7 +17,8 @@
},
{
"name": "Seiberth, Luz Christopher",
- "affiliation": "TextRefs"
+ "affiliation": "TextRefs",
+ "orcid": "0000-0002-5606-0964"
}
],
"keywords": [
diff --git a/CITATION.cff b/CITATION.cff
index 55cc761..ee1ee82 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -13,8 +13,8 @@ authors:
- family-names: Seiberth
given-names: Luz Christopher
email: luz@seiberth.de
+ orcid: 'https://orcid.org/0000-0002-5606-0964'
affiliation: TextRefs
- # ORCID: TBD
repository-code: 'https://github.com/textrefs/textrefs.org'
url: 'https://textrefs.org'
license:
diff --git a/github-profile b/github-profile
index 5fbd182..9b707b9 160000
--- a/github-profile
+++ b/github-profile
@@ -1 +1 @@
-Subproject commit 5fbd182911f37ff025647c874133b7ce491bbf9e
+Subproject commit 9b707b984843736e0d917aea1060d1def560634e
From 9c9f2b5206424ae9069fcd266a5411d271d3c365 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
Date: Sun, 5 Jul 2026 21:13:05 +0200
Subject: [PATCH 13/24] feat(spec)!: seed CanonicalReference UUIDs from the
semantic identity tuple (ADR-0002) (#21)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat(spec)!: seed CanonicalReference UUIDs from the semantic identity tuple
Adopt ADR-0002 (fixes #15): reference UUIDs are computable offline from
(work_key, citation_system_key, locator); normalization_version is
removed from the data model entirely. Citation-system profiles now
define canonical locator forms — non-canonical spellings are rejected at
validation time, never folded — and a profile change that alters an
accepted locator is a compatibility event (migration, breaking release,
or new citation_system_key), not a version bump.
- Zod schemas: drop normalization_version (reference + system), drop
examples, add required CitationSystem.description; drop unused SemVer
- compile/validate: 3-field LF-joined seed on both sides; systems emit
description instead of normalization_version/examples
- JSON-LD context + OpenAPI: same shape change
(description = dcterms:description)
- spec docs: identifier-syntax gains a purpose-of-determinism section
and the canonicalization-change compatibility rule; system-profiles
gains the canonical-locator-form section (relates to #13)
- doc example UUIDs recomputed for the 3-field seed
- data submodule → textrefs/registry#5 (systems gain description, lose
normalization_version/examples)
Co-Authored-By: Claude Fable 5
* docs(spec): align conformance item 6 and get-started checklist with examples removal
The CitationSystem contract no longer carries examples.valid/invalid;
conformance now requires description + a valid ECMAScript locator_regex.
Co-Authored-By: Claude Fable 5
* chore(data): bump submodule to registry review fixes (5337e71)
modified bumped on all systems for the shape change; bible description
aligned with its locator_regex (Copilot review on registry#5).
Co-Authored-By: Claude Fable 5
* docs(spec): align quoted bible description with updated registry text
Co-Authored-By: Claude Fable 5
---------
Co-authored-by: Moritz Mähr <14755525+maehr@users.noreply.github.com>
Co-authored-by: Claude Fable 5
---
api/openapi.yaml | 39 +---------
data | 2 +-
.../ADR-0002-uuid-seed-semantic-identity.md | 74 +++++++++++++++++++
public/contexts/v1.jsonld | 7 +-
scripts/compile.ts | 18 +----
scripts/validate-data.ts | 7 +-
src/content/docs/get-started/authoring.md | 8 +-
src/content/docs/get-started/how-it-works.md | 17 ++---
.../mappings-and-resolver-targets.md | 7 +-
src/content/docs/get-started/url-layout.md | 6 +-
.../docs/standard/identifier-syntax.md | 20 ++---
src/content/docs/standard/json-ld.md | 7 +-
src/content/docs/standard/specification.md | 42 ++++-------
src/content/docs/standard/system-profiles.md | 30 +++-----
src/lib/registry.fixture.ts | 4 +-
src/pages/id/ref/[uuid]/index.astro | 1 -
src/pages/id/system/[key]/index.astro | 26 +------
standard/schema/canonical-reference.ts | 3 +-
standard/schema/citation-system.ts | 28 +------
standard/schema/common.ts | 4 -
20 files changed, 148 insertions(+), 202 deletions(-)
create mode 100644 decisions/ADR-0002-uuid-seed-semantic-identity.md
diff --git a/api/openapi.yaml b/api/openapi.yaml
index ffc9d79..37abd41 100644
--- a/api/openapi.yaml
+++ b/api/openapi.yaml
@@ -180,9 +180,6 @@ components:
IsoDate:
type: string
pattern: '^\d{4}-\d{2}-\d{2}$'
- SemVer:
- type: string
- pattern: '^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$'
Status:
type: string
enum: [candidate, active, deprecated, withdrawn, blocked]
@@ -209,16 +206,7 @@ components:
allOf:
- $ref: '#/components/schemas/AdminMetadata'
- type: object
- required:
- [
- id,
- key,
- type,
- preferred_label,
- normalization_version,
- locator_regex,
- examples,
- ]
+ required: [id, key, type, preferred_label, description, locator_regex]
properties:
id:
type: string
@@ -226,20 +214,8 @@ components:
key: { $ref: '#/components/schemas/FlatKey' }
type: { type: string, enum: [CitationSystem] }
preferred_label: { type: string, minLength: 1 }
- normalization_version: { $ref: '#/components/schemas/SemVer' }
+ description: { type: string, minLength: 1 }
locator_regex: { type: string, minLength: 1 }
- examples:
- type: object
- required: [valid, invalid]
- properties:
- valid:
- type: array
- items: { type: string }
- minItems: 1
- invalid:
- type: array
- items: { type: string }
- minItems: 1
ResolverTargetEntry:
type: object
required: [url, access]
@@ -262,15 +238,7 @@ components:
- $ref: '#/components/schemas/AdminMetadata'
- type: object
required:
- [
- id,
- type,
- work_key,
- citation_system_key,
- locator,
- normalization_version,
- resolver_targets,
- ]
+ [id, type, work_key, citation_system_key, locator, resolver_targets]
properties:
id:
type: string
@@ -279,7 +247,6 @@ components:
work_key: { $ref: '#/components/schemas/FlatKey' }
citation_system_key: { $ref: '#/components/schemas/FlatKey' }
locator: { type: string, minLength: 1 }
- normalization_version: { $ref: '#/components/schemas/SemVer' }
resolver_targets:
type: array
default: []
diff --git a/data b/data
index 36cae56..5337e71 160000
--- a/data
+++ b/data
@@ -1 +1 @@
-Subproject commit 36cae56593807e6e5ebbdd4eb96878eb74229f9d
+Subproject commit 5337e712f42b510ae2515549080b3cbb60aec254
diff --git a/decisions/ADR-0002-uuid-seed-semantic-identity.md b/decisions/ADR-0002-uuid-seed-semantic-identity.md
new file mode 100644
index 0000000..e738985
--- /dev/null
+++ b/decisions/ADR-0002-uuid-seed-semantic-identity.md
@@ -0,0 +1,74 @@
+# ADR-0002: Seed CanonicalReference UUIDs from the semantic identity tuple
+
+- **Status:** Proposed
+- **Date:** 2026-07-05
+- **Deciders:** @maehr
+- **Tags:** spec
+
+## Context and problem statement
+
+The `CanonicalReference` UUID v5 seed included four fields: `work_key`, `citation_system_key`, `locator`, and `normalization_version`, with `normalization_version` fixed at minting time. Issue [#15](https://github.com/textrefs/textrefs.org/issues/15) observes the consequence: a third party holding only `(work_key, citation_system_key, locator)` cannot compute the registry UUID, because they cannot know which `normalization_version` was in force when that reference was minted. They must look the reference up first — at which point they already have the IRI. "Deterministic" therefore did not mean "offline-computable from semantic fields", and the spec was silent about which purpose determinism actually serves.
+
+TextRefs has not yet published authoritative reference data. UUIDs are computed at compile time from enumerated YAML source; none are stored in the registry. Changing the seed now recomputes identifiers at zero migration cost; changing it after publication would be a registry-wide tombstone event.
+
+## Decision drivers
+
+- The intuitive public contract for a registry of canonical references: same work + same citation system + same canonical locator = same reference ID.
+- Offline computability for third-party tooling that holds the canonical fields.
+- Pre-v1.0.0 and pre-data: a breaking seed change is still nearly free (precedent: ADR-0001).
+- An identity field whose contract needs a lookup to explain (issue #15) breeds the next round of confusion; a version field kept "just as metadata" would do the same.
+- Multiple regex-valid spellings of one passage would mint permanent identity splits (issue [#13](https://github.com/textrefs/textrefs.org/issues/13)), so removing the version discriminator must be paired with strict canonical locator forms.
+
+## Considered options
+
+1. **Keep the 4-field seed; declare determinism's purpose as compiler/mirror reproducibility** — zero migration; documents that IDs are not offline-computable. (Issue #15's original recommendation.)
+2. **Remove `normalization_version` from the seed, keep loose normalization** — offline-computable IDs, but silently re-opens the `John.3.16` / `john.3.16` identity-split class.
+3. **Keep the seed; tell clients to compute with the current version and fall back to lookup** — false sense of offline computability; silent mis-mints.
+4. **Revised option 2: 3-field seed + strict canonical locator forms + drop `normalization_version` from the data model** — offline-computable IDs with the split window closed by profile strictness instead of a version discriminator.
+
+## Decision
+
+We choose **Option 4**. Because TextRefs has not yet published authoritative reference data, canonical-reference UUIDs become computable from the semantic identity tuple: `work_key`, `citation_system_key`, and the canonical `locator`, LF-joined in that order (namespace UUID unchanged).
+
+`normalization_version` is removed from the data model entirely — not demoted to metadata. Once outside the seed it would version nothing that matters: a normalization change that alters any accepted locator is compatibility-sensitive by the rules below regardless of any version label, and rule evolution that does not alter locators is visible through `modified`. Keeping a vestigial version field would reproduce the confusion that motivated issue #15.
+
+The accompanying rules that make the 3-field seed safe:
+
+- **Citation-system profiles define canonical forms.** Each profile defines exactly one canonical spelling per reference point, in `locator_regex` (machine-checkable constraints, including ASCII digit and case forms — issue #13) and in the new required `description` field (prose constraints).
+- **Non-canonical locators are rejected at validation time**, never silently folded into the canonical form. The compiler's regex gate stays a hard error.
+- **The enumerated registry data is the canon.** A locator is canonical and attested because a curated work record enumerates it; `locator_regex` is the machine-checkable floor beneath curation, not the definition of validity.
+- **Canonicalization changes are compatibility events.** A profile change that would alter the canonical spelling of any accepted locator is a pre-1.0 registry migration, a breaking registry release, or — when the distinction is genuinely semantic — a new `citation_system_key`. Never a silent change.
+
+Two simplifications ride along, keeping the `CitationSystem` shape honest after the field removal:
+
+- The `examples.valid` / `examples.invalid` block is removed. Its only job was self-testing `locator_regex`; the compiler already tests the regex against every enumerated locator of every work, a strictly larger test set.
+- The explanatory YAML comments in system files move into the required `description` field (`dcterms:description` in the JSON-LD context), so the prose part of the profile contract is published rather than buried in source comments.
+
+## Consequences
+
+### Positive
+
+- Same work + same citation system + same canonical locator = same reference ID, computable offline.
+- No identity-bearing field whose value depends on registry state at minting time.
+- Profile discipline (canonical forms, rejection over folding) is forced now, before data exists, instead of compensated for later.
+- Leaner `CitationSystem` records: one label, one description, one regex.
+
+### Negative / trade-offs
+
+- Every compiled reference UUID changes. Acceptable now (nothing published, nothing stored); it would not be later.
+- Without a version discriminator, locator-affecting profile changes have no soft path — by design, but it makes profile authors' first regex more consequential.
+- The regexes shipped today are laxer than the canonical-form rules demand (Bekker leading zeros, Bible case variants); until #13 lands, curation of the enumerated data is the only guard in those gaps.
+
+### Follow-up actions
+
+- [ ] Update spec (`specification.md` §§5, 7, 8, 11, 13, 14; `identifier-syntax.md`; `system-profiles.md`; `json-ld.md`) and get-started docs.
+- [ ] Update the v1 JSON-LD context (drop `normalization_version`, `examples`, `valid`, `invalid`; add `description`).
+- [ ] Update Zod schemas, compile/validate pipeline, in-tree fixture, and record pages.
+- [ ] Migrate `data/systems/*.yaml` in `textrefs/registry` and bump the `data/` submodule pointer.
+- [ ] Tighten per-profile `locator_regex` canonical digit/case forms under issue #13 (textrefs/registry#2, textrefs/registry#3).
+
+## Links
+
+- Related ADRs: ADR-0001 (precedent for a pre-v1.0.0 breaking change)
+- Related issues / PRs: textrefs/textrefs.org#15, textrefs/textrefs.org#13, textrefs/textrefs.org#9, textrefs/registry#2, textrefs/registry#3
+- External references: [RFC 9562 — UUID v5](https://www.rfc-editor.org/rfc/rfc9562)
diff --git a/public/contexts/v1.jsonld b/public/contexts/v1.jsonld
index 19319f4..04fb67a 100644
--- a/public/contexts/v1.jsonld
+++ b/public/contexts/v1.jsonld
@@ -13,6 +13,7 @@
"MappingAssertion": "tr:MappingAssertion",
"key": "tr:key",
"preferred_label": "skos:prefLabel",
+ "description": "dcterms:description",
"creators": "schema:creator",
"kind": "tr:creatorKind",
"family": "schema:familyName",
@@ -25,7 +26,6 @@
"work_key": "tr:workKey",
"citation_system_key": "tr:citationSystemKey",
"locator": "tr:locator",
- "normalization_version": "tr:normalizationVersion",
"status": "tr:status",
"source": "dcterms:source",
"created": {
@@ -76,9 +76,6 @@
"@id": "tr:lastChecked",
"@type": "xsd:date"
},
- "locator_regex": "tr:locatorRegex",
- "examples": "tr:examples",
- "valid": "tr:validExample",
- "invalid": "tr:invalidExample"
+ "locator_regex": "tr:locatorRegex"
}
}
diff --git a/scripts/compile.ts b/scripts/compile.ts
index b45fb12..8a8c251 100644
--- a/scripts/compile.ts
+++ b/scripts/compile.ts
@@ -18,7 +18,6 @@ import {
const REFERENCE_NS = 'b1a3670e-2ac7-544c-a1b9-396e0dc193f7';
const MAPPING_NS = 'f16bb214-4241-549d-ad41-7b011f02befb';
-const NORMALIZATION_VERSION = '1.0.0';
const projectRoot = resolve(process.cwd());
const dataRoot = join(projectRoot, 'data');
@@ -158,9 +157,8 @@ type WorkSource = {
type SystemSource = {
key: string;
preferred_label: string;
- normalization_version: string;
+ description: string;
locator_regex: string;
- examples: { valid: string[]; invalid: string[] };
status: string;
created: string;
modified: string;
@@ -309,9 +307,8 @@ function referenceUuid(
workKey: string,
systemKey: string,
locator: string,
- normalizationVersion: string,
): string {
- const seed = [workKey, systemKey, locator, normalizationVersion].join('\n');
+ const seed = [workKey, systemKey, locator].join('\n');
return uuidv5(seed, REFERENCE_NS);
}
@@ -371,9 +368,8 @@ export function compileRegistry(): CompiledRegistry {
key,
type: 'CitationSystem' as const,
preferred_label: src.preferred_label,
- normalization_version: src.normalization_version,
+ description: src.description,
locator_regex: src.locator_regex,
- examples: src.examples,
status: src.status,
created: src.created,
modified: src.modified,
@@ -486,19 +482,13 @@ export function compileRegistry(): CompiledRegistry {
const entry = buildResolverEntry(resolver, vars);
if (entry) targets.push(entry);
}
- const uuid = referenceUuid(
- workKey,
- systemKey,
- locator,
- NORMALIZATION_VERSION,
- );
+ const uuid = referenceUuid(workKey, systemKey, locator);
const record = {
id: `https://textrefs.org/id/ref/${uuid}`,
type: 'CanonicalReference' as const,
work_key: workKey,
citation_system_key: systemKey,
locator,
- normalization_version: NORMALIZATION_VERSION,
resolver_targets: targets,
status: src.work.status,
created: src.work.created,
diff --git a/scripts/validate-data.ts b/scripts/validate-data.ts
index 956656f..5813a2d 100644
--- a/scripts/validate-data.ts
+++ b/scripts/validate-data.ts
@@ -51,12 +51,7 @@ for (const ref of registry.references) {
reportIssue(`ref/${ref.work_key}/${ref.locator}`, r.error.issues);
continue;
}
- const seed = [
- ref.work_key,
- ref.citation_system_key,
- ref.locator,
- ref.normalization_version,
- ].join('\n');
+ const seed = [ref.work_key, ref.citation_system_key, ref.locator].join('\n');
const expected = `https://textrefs.org/id/ref/${uuidv5(seed, REFERENCE_NS)}`;
if (ref.id !== expected) {
console.error(
diff --git a/src/content/docs/get-started/authoring.md b/src/content/docs/get-started/authoring.md
index 45c45d8..71a269d 100644
--- a/src/content/docs/get-started/authoring.md
+++ b/src/content/docs/get-started/authoring.md
@@ -235,7 +235,10 @@ A citation system declares its locator format once and is reused by every work t
# data/systems/dhammapada-chapter-verse.yaml
key: dhammapada-chapter-verse
preferred_label: Dhammapada chapter-and-verse
-normalization_version: 1.0.0
+description: >-
+ Dhammapada cited by chapter and verse (`chapter.verse`), e.g. 1.1 through
+ 26.41, following the standard Pali Text Society numbering of 423 verses
+ in 26 chapters.
locator_regex: '^(?[1-9]|1[0-9]|2[0-6])\.(?[1-9][0-9]*)$'
# Optional: per-chapter verse counts. When present, the compiler exposes
# `{verseGlobal}` (cumulative 1..N) to URL templates of works using this system.
@@ -268,9 +271,6 @@ chapter_sizes:
23,
41,
]
-examples:
- valid: ['1.1', '1.20', '8.3', '26.41']
- invalid: ['0.1', '27.1', '1', '1.0']
status: candidate
created: 2026-05-31
modified: 2026-06-01
diff --git a/src/content/docs/get-started/how-it-works.md b/src/content/docs/get-started/how-it-works.md
index 3882064..86cac40 100644
--- a/src/content/docs/get-started/how-it-works.md
+++ b/src/content/docs/get-started/how-it-works.md
@@ -29,22 +29,21 @@ For a citation such as "Plato, _Republic_ 514a", a registry contributor or parse
1. Identify the work: `plato.republic`.
2. Identify the citation system: `stephanus`.
3. Normalize the locator according to that citation system: `514a`.
-4. Mint the `CanonicalReference` ID from the tuple `work_key`, `citation_system_key`, `locator`, and `normalization_version`.
+4. Mint the `CanonicalReference` ID from the tuple `work_key`, `citation_system_key`, and `locator`.
The result is a persistent TextRefs URI for the cited point itself. That URI stays stable if a website changes its URLs, a library adds a new scan, a translation appears, or a mapping is corrected.
## Example: Plato, Republic 514a
-The identity is the Stephanus reference, not any one Greek text, translation, or platform page. The compiled registry record for [`plato.republic` `514a`](/id/ref/884e8b51-b9cc-5f4b-9e49-60c636c0cd1a/) is:
+The identity is the Stephanus reference, not any one Greek text, translation, or platform page. The compiled registry record for [`plato.republic` `514a`](/id/ref/dc799d4b-9b17-5d76-85aa-dfd001c5321d/) is:
```json
{
- "id": "https://textrefs.org/id/ref/884e8b51-b9cc-5f4b-9e49-60c636c0cd1a",
+ "id": "https://textrefs.org/id/ref/dc799d4b-9b17-5d76-85aa-dfd001c5321d",
"type": "CanonicalReference",
"work_key": "plato.republic",
"citation_system_key": "stephanus",
"locator": "514a",
- "normalization_version": "1.0.0",
"resolver_targets": [
{
"url": "https://www.perseus.tufts.edu/hopper/text?doc=Plat.+Rep.+514a",
@@ -63,16 +62,15 @@ The identity is the Stephanus reference, not any one Greek text, translation, or
}
```
-When reading locations exist, they are embedded on the reference itself, one entry per provider or translation. For example, the compiled registry record for [`new-testament` `John.3.16`](/id/ref/59a2d83f-6aff-5fbf-b8f7-b243c3ed0594/) contains a resolver target to STEP Bible:
+When reading locations exist, they are embedded on the reference itself, one entry per provider or translation. For example, the compiled registry record for [`new-testament` `John.3.16`](/id/ref/b6438d55-f3f2-5fc7-ab40-4f582f8774c3/) contains a resolver target to STEP Bible:
```json
{
- "id": "https://textrefs.org/id/ref/59a2d83f-6aff-5fbf-b8f7-b243c3ed0594",
+ "id": "https://textrefs.org/id/ref/b6438d55-f3f2-5fc7-ab40-4f582f8774c3",
"type": "CanonicalReference",
"work_key": "new-testament",
"citation_system_key": "bible-book-chapter-verse",
"locator": "John.3.16",
- "normalization_version": "1.0.0",
"resolver_targets": [
{
"url": "https://www.stepbible.org/?q=version=SBLG|reference=John.3.16",
@@ -112,15 +110,14 @@ Adding a resolver target adds one entry to `resolver_targets`; adding a Wikidata
## Example: John.3.16
-For a heavily translated work, many locations can share one reference identity. The current data-backed example is [`new-testament` `John.3.16`](/id/ref/59a2d83f-6aff-5fbf-b8f7-b243c3ed0594/):
+For a heavily translated work, many locations can share one reference identity. The current data-backed example is [`new-testament` `John.3.16`](/id/ref/b6438d55-f3f2-5fc7-ab40-4f582f8774c3/):
```json
{
"type": "CanonicalReference",
"work_key": "new-testament",
"citation_system_key": "bible-book-chapter-verse",
- "locator": "John.3.16",
- "normalization_version": "1.0.0"
+ "locator": "John.3.16"
}
```
diff --git a/src/content/docs/get-started/mappings-and-resolver-targets.md b/src/content/docs/get-started/mappings-and-resolver-targets.md
index c271d82..0fbfa3b 100644
--- a/src/content/docs/get-started/mappings-and-resolver-targets.md
+++ b/src/content/docs/get-started/mappings-and-resolver-targets.md
@@ -69,7 +69,6 @@ Each entry records what a reader needs to understand the link: `language` (BCP 4
"work_key": "dhammapada",
"citation_system_key": "dhammapada-chapter-verse",
"locator": "1.1",
- "normalization_version": "1.0.0",
"resolver_targets": [
{
"url": "https://en.wikisource.org/wiki/Dhammapada_(Muller)#Chapter_I:_The_Twin-Verses",
@@ -92,7 +91,7 @@ Contributors author resolver targets as URL templates, not raw URLs — see [Aut
## Good first candidates
-The best TextRefs candidates are works with established, edition-independent citation systems. The examples below are illustrative; each actual profile still needs documented normalization rules, valid examples, invalid examples, and review.
+The best TextRefs candidates are works with established, edition-independent citation systems. The examples below are illustrative; each actual profile still needs a documented canonical locator form, a `locator_regex`, and review.
| Area | Citation example | Citation system | Notes |
| ---------- | ---------------------------------------- | -------------------- | ------------------------------------------------------------- |
@@ -125,8 +124,8 @@ An author's name alone is not a `Work`. For example, "Confucius" is an authority
Before proposing mappings or resolver targets, check that:
-- the cited passage has a clear `Work`, `CitationSystem`, and normalized locator;
-- the citation system has valid and invalid examples;
+- the cited passage has a clear `Work`, `CitationSystem`, and canonical locator;
+- the citation system documents its canonical locator form and declares a `locator_regex`;
- each `MappingAssertion` subject is a Work IRI and its target identifies a textual resource;
- each `exactMatch` is precise enough to survive review;
- each `resolver_targets` entry has a dereferenceable URL and honest access metadata;
diff --git a/src/content/docs/get-started/url-layout.md b/src/content/docs/get-started/url-layout.md
index 24c1b53..cab425b 100644
--- a/src/content/docs/get-started/url-layout.md
+++ b/src/content/docs/get-started/url-layout.md
@@ -23,8 +23,8 @@ In one line:
Plato's _Republic_ 514a — the Stephanus passage where Socrates begins the Allegory of the Cave — is one canonical reference. Here is what each prefix gives you for it:
- **Canonical identifier** — the URL you cite, link from a paper, or paste into a tool:
- - `https://textrefs.org/id/ref/884e8b51-b9cc-5f4b-9e49-60c636c0cd1a/` (HTML for browsers)
- - `https://textrefs.org/id/ref/884e8b51-b9cc-5f4b-9e49-60c636c0cd1a.json` (JSON-LD for machines)
+ - `https://textrefs.org/id/ref/dc799d4b-9b17-5d76-85aa-dfd001c5321d/` (HTML for browsers)
+ - `https://textrefs.org/id/ref/dc799d4b-9b17-5d76-85aa-dfd001c5321d.json` (JSON-LD for machines)
- **Browseable index** — the registry's human entry point, where readers find works, citation systems, and (via the work page) every reference:
- `https://textrefs.org/reg/`
- **Short alias** — a memorable, hand-typeable shortcut that redirects to the canonical URL:
@@ -37,7 +37,7 @@ Plato's _Republic_ 514a — the Stephanus passage where Socrates begins the Alle
There is no `Accept`-header content negotiation. Every HTML record page advertises its JSON-LD sibling in the document head:
```html
-
+
```
A client either reads that `` tag, or simply appends `.json` to the canonical URL. The JSON payload carries the JSON-LD `@context` at [`/contexts/v1.jsonld`](/contexts/v1.jsonld) and is valid JSON-LD by content.
diff --git a/src/content/docs/standard/identifier-syntax.md b/src/content/docs/standard/identifier-syntax.md
index 2835904..3c0dd8b 100644
--- a/src/content/docs/standard/identifier-syntax.md
+++ b/src/content/docs/standard/identifier-syntax.md
@@ -8,6 +8,10 @@ sidebar:
TextRefs canonical-reference identifiers use deterministic UUID v5 generation. The algorithm is intentionally strict so that independent implementations produce identical identifiers from the same input.
+## Purpose of determinism
+
+Deterministic identity means anyone holding the semantic identity tuple — `work_key`, `citation_system_key`, and the canonical `locator` — can compute the registry UUID offline, without a lookup. The registry remains authoritative for which locators are canonical and attested: a computed UUID identifies a reference, but only a registered `CanonicalReference` makes it a valid TextRefs reference (see [Specification §14](/standard/specification/#14-validation-requirements)). See [ADR-0002](https://github.com/textrefs/textrefs.org/blob/main/decisions/ADR-0002-uuid-seed-semantic-identity.md) for the rationale.
+
## Namespace
The TextRefs reference namespace UUID is:
@@ -20,21 +24,20 @@ This namespace is derived from `uuidv5(uuid.NAMESPACE_DNS, "textrefs.org/referen
## Seed sequence
-The UUID seed string is the following four-field sequence, in this exact order:
+The UUID seed string is the following three-field sequence, in this exact order:
```text
work_key
citation_system_key
locator
-normalization_version
```
Serialization rules:
- Encode the seed as UTF-8.
-- Join the four values with a single line feed character, `U+000A`.
+- Join the three values with a single line feed character, `U+000A`.
- Use each field exactly as normalized, with no leading or trailing whitespace.
-- End the seed after `normalization_version`, with no final trailing line feed.
+- End the seed after `locator`, with no final trailing line feed.
- Use the registry key fields themselves; labels, URIs, aliases, and external identifiers belong in metadata or mappings.
- Each field MUST already be normalized by its owning profile before UUID generation.
@@ -61,7 +64,8 @@ Deterministic identifiers depend on byte-identical seed strings. Before validati
- `locator` MUST NOT contain leading or trailing whitespace, control characters, or internal whitespace unless the citation-system profile explicitly allows it.
- Implementations MUST NOT apply NFKC, case folding, digit folding, punctuation folding, transliteration, or script conversion unless the citation-system profile explicitly defines that rule.
- Profiles for mixed-script locators MUST state the allowed scripts and enforce them through `locator_regex`.
-- Any change to locator normalization that can change a normalized locator MUST change the citation system's `normalization_version`.
+- The `locator` stored on a `CanonicalReference` is the canonical spelling defined by the citation-system profile. Non-canonical spellings MUST be rejected at validation time, never silently folded into the canonical form.
+- A profile change that would alter the canonical spelling of any accepted locator changes reference identity. It MUST be handled as a registry migration (pre-1.0), a breaking registry release, or — when the distinction is genuinely semantic — a new `citation_system_key`. It MUST NOT be applied silently.
The seed bytes used for UUID v5 generation are ASCII-restricted (keys) and NFC-normalized UTF-8 (locators). This is independent of whether downstream TextRefs identifiers are expressed as URIs ([RFC 3986](https://www.rfc-editor.org/rfc/rfc3986)) or IRIs ([RFC 3987](https://www.rfc-editor.org/rfc/rfc3987)).
@@ -73,7 +77,6 @@ Input tuple:
work_key = plato.republic
citation_system_key = stephanus
locator = 514a
-normalization_version = 1.0.0
```
Seed string:
@@ -82,19 +85,18 @@ Seed string:
plato.republic
stephanus
514a
-1.0.0
```
Result:
```text
-884e8b51-b9cc-5f4b-9e49-60c636c0cd1a
+dc799d4b-9b17-5d76-85aa-dfd001c5321d
```
Canonical URI:
```text
-https://textrefs.org/id/ref/884e8b51-b9cc-5f4b-9e49-60c636c0cd1a
+https://textrefs.org/id/ref/dc799d4b-9b17-5d76-85aa-dfd001c5321d
```
## MappingAssertion seed
diff --git a/src/content/docs/standard/json-ld.md b/src/content/docs/standard/json-ld.md
index 6c5398a..077d195 100644
--- a/src/content/docs/standard/json-ld.md
+++ b/src/content/docs/standard/json-ld.md
@@ -51,6 +51,7 @@ Use `exactMatch` only when the mapped object identifies the same reference with
"MappingAssertion": "tr:MappingAssertion",
"key": "tr:key",
"preferred_label": "skos:prefLabel",
+ "description": "dcterms:description",
"creators": "schema:creator",
"kind": "tr:creatorKind",
"family": "schema:familyName",
@@ -60,7 +61,6 @@ Use `exactMatch` only when the mapped object identifies the same reference with
"work_key": "tr:workKey",
"citation_system_key": "tr:citationSystemKey",
"locator": "tr:locator",
- "normalization_version": "tr:normalizationVersion",
"status": "tr:status",
"source": "dcterms:source",
"created": { "@id": "dcterms:created", "@type": "xsd:date" },
@@ -81,10 +81,7 @@ Use `exactMatch` only when the mapped object identifies the same reference with
"license": "dcterms:license",
"license_url": { "@id": "dcterms:license", "@type": "@id" },
"last_checked": { "@id": "tr:lastChecked", "@type": "xsd:date" },
- "locator_regex": "tr:locatorRegex",
- "examples": "tr:examples",
- "valid": "tr:validExample",
- "invalid": "tr:invalidExample"
+ "locator_regex": "tr:locatorRegex"
}
}
```
diff --git a/src/content/docs/standard/specification.md b/src/content/docs/standard/specification.md
index 0ad3461..c432725 100644
--- a/src/content/docs/standard/specification.md
+++ b/src/content/docs/standard/specification.md
@@ -27,7 +27,7 @@ A dataset conforms to the TextRefs Standard if it satisfies all of the following
3. Every `Work.key` and `CitationSystem.key` is a flat, stable key that occupies one URI path segment.
4. Every `CanonicalReference` points to one known `Work` and one known `CitationSystem`.
5. Every `CanonicalReference.locator` validates syntactically against the referenced `CitationSystem` and semantically by being a registered reference point for the referenced `Work`.
-6. Every `CitationSystem` declares valid and invalid examples for automated tests.
+6. Every `CitationSystem` declares a `description` of its citation tradition and a `locator_regex` that is a valid ECMAScript regular expression.
7. Every dereferenceable location is represented as an entry in the `resolver_targets` array of its `CanonicalReference`, and every external identifier or cross-reference equivalence through a `MappingAssertion`.
8. Every registry object includes administrative metadata.
9. Registry records contain identifiers, metadata, mappings, provenance, and resolver targets rather than primary text content.
@@ -74,15 +74,14 @@ classDiagram
+URI id
+string key
+string preferred_label
+ +string description
+string locator_regex
- +string normalization_version
}
class CanonicalReference {
+URI id
+string work_key
+string citation_system_key
+string locator
- +string normalization_version
+ResolverTargetEntry[] resolver_targets
}
class ResolverTargetEntry {
@@ -146,33 +145,28 @@ A `CitationSystem.key` is a single flat registry key for a locator notation and
"key": "bible-book-chapter-verse",
"type": "CitationSystem",
"preferred_label": "Bible book-chapter-verse (OSIS-style)",
- "normalization_version": "1.0.0",
+ "description": "OSIS-style locator: `Book.Chapter.Verse`. The book component accepts OSIS-compatible book identifiers (letters, digits, and underscores, starting with a letter; no spaces). Both the Hebrew Bible and the New Testament cite with this grammar.",
"locator_regex": "^(?[A-Za-z][A-Za-z0-9_]*)\\.(?[1-9][0-9]*)\\.(?[1-9][0-9]*)$",
- "examples": {
- "valid": ["Genesis.1.1", "Psalms.23.1", "Matthew.5.3"],
- "invalid": ["Genesis.0.1", "Genesis.1", "1.1.1", "Genesis 1:1"]
- },
"status": "candidate",
"created": "2026-05-31",
"modified": "2026-05-31"
}
```
-Required: `id`, `key`, `type` (`CitationSystem`), `preferred_label`, `normalization_version`, `locator_regex`, `examples.valid`, `examples.invalid`, plus administrative metadata. The `id` MUST be a persistent TextRefs HTTP URI of the form `https://textrefs.org/id/system/{key}`, where `{key}` is one flat key and occupies exactly one URI path segment.
+Required: `id`, `key`, `type` (`CitationSystem`), `preferred_label`, `description`, `locator_regex`, plus administrative metadata. The `id` MUST be a persistent TextRefs HTTP URI of the form `https://textrefs.org/id/system/{key}`, where `{key}` is one flat key and occupies exactly one URI path segment.
+- `description` documents the citation tradition and its canonical locator form in prose, including any canonical-form rules that cannot be expressed in `locator_regex`.
- `locator_regex` MUST be a valid ECMAScript regular expression.
- `locator_regex` provides machine-checkable pre-validation for locator shape only; it need not fully describe citation systems whose valid references cannot be expressed completely as a regular language.
- Citation systems SHOULD use an anchored `locator_regex` when the pattern is intended to describe the full locator string.
- Regex success does not by itself prove that a reference point exists in a work.
-- `normalization_version` MUST use semantic versioning.
-- `examples.valid` MUST all match `locator_regex`; `examples.invalid` MUST all fail it.
- Unicode handling for keys and locators MUST follow [Identifier syntax](/standard/identifier-syntax/#unicode-normalization).
-- A pull request that adds or changes a citation system MUST include the profile, valid examples, and invalid examples. See [Citation-system profiles](/standard/system-profiles/).
+- A pull request that adds or changes a citation system MUST include the full profile record. See [Citation-system profiles](/standard/system-profiles/).
- A `CanonicalReference` links to its citation system through `citation_system_key`. JSON-LD serializations MAY additionally expose that relation with `skos:inScheme`.
## 8. CanonicalReference
-A `CanonicalReference` represents one atomized, **language-independent** reference point, identified by combining a work, a citation system, a normalized locator, and a normalization version. It also carries the set of dereferenceable external locations for that reference as an embedded `resolver_targets` array (see [§9](#9-embedded-resolver-targets)).
+A `CanonicalReference` represents one atomized, **language-independent** reference point, identified by combining a work, a citation system, and a canonical locator. It also carries the set of dereferenceable external locations for that reference as an embedded `resolver_targets` array (see [§9](#9-embedded-resolver-targets)).
```json
{
@@ -181,7 +175,6 @@ A `CanonicalReference` represents one atomized, **language-independent** referen
"work_key": "new-testament",
"citation_system_key": "bible-book-chapter-verse",
"locator": "John.3.16",
- "normalization_version": "1.0.0",
"resolver_targets": [
{
"url": "https://www.stepbible.org/?q=version=SBLG|reference=John.3.16",
@@ -198,13 +191,13 @@ A `CanonicalReference` represents one atomized, **language-independent** referen
}
```
-Required: `id`, `type` (`CanonicalReference`), `work_key`, `citation_system_key`, `locator`, `normalization_version`, `resolver_targets` (MAY be empty), plus administrative metadata.
+Required: `id`, `type` (`CanonicalReference`), `work_key`, `citation_system_key`, `locator`, `resolver_targets` (MAY be empty), plus administrative metadata.
- `work_key` MUST reference a known `Work`; `citation_system_key` MUST reference a known `CitationSystem`.
- `work_key` and `citation_system_key` MUST be treated as opaque flat keys. Implementations MUST NOT infer author, corpus, title, hierarchy, or resolver behaviour by splitting either key.
-- `locator` MUST match the system's `locator_regex`; additional profile-specific validation MAY be required for systems that are not fully regex-checkable.
+- `locator` MUST be the canonical spelling defined by the citation-system profile and MUST match the system's `locator_regex`; additional profile-specific validation MAY be required for systems that are not fully regex-checkable. Non-canonical spellings MUST be rejected at validation time, never silently normalized (see [Identifier syntax](/standard/identifier-syntax/#unicode-normalization)).
- An accepted `CanonicalReference` MUST represent an attested reference point for the referenced `Work` under the referenced `CitationSystem`.
-- `normalization_version` is part of the reference's identity and is fixed when the reference is minted; it records the normalization in force at that time and need not equal the citation system's current `normalization_version`. Its correctness is verified by the deterministic identifier (see [§14](#14-validation-requirements) and [Identifier syntax](/standard/identifier-syntax/)).
+- A profile change that would alter the canonical spelling of any accepted locator changes reference identity and MUST be handled as a registry migration, a breaking registry release, or a new `citation_system_key` (see [Identifier syntax](/standard/identifier-syntax/#unicode-normalization)).
- The `id` MUST be generated deterministically per [Identifier syntax](/standard/identifier-syntax/); its UUID component is the deterministic seed output.
- `resolver_targets` MUST validate per [§9](#9-embedded-resolver-targets).
@@ -270,7 +263,7 @@ TextRefs identifiers MUST be persistent HTTP URIs ([RFC 3986](https://www.rfc-ed
`Work` identifiers MUST use `https://textrefs.org/id/work/{key}` and `CitationSystem` identifiers MUST use `https://textrefs.org/id/system/{key}`. In both cases `{key}` is the complete flat key and MUST NOT contain additional path segments. For example, `https://textrefs.org/id/work/plato.republic` is valid; `https://textrefs.org/id/work/plato/republic` is not.
-A `CanonicalReference` identifier MUST be generated deterministically. The identity seed MUST include `work_key`, `citation_system_key`, `locator`, and `normalization_version`, in that order (see [Identifier syntax](/standard/identifier-syntax/)).
+A `CanonicalReference` identifier MUST be generated deterministically. The identity seed MUST include `work_key`, `citation_system_key`, and `locator`, in that order (see [Identifier syntax](/standard/identifier-syntax/)).
A `MappingAssertion` identifier MUST be generated deterministically from `subject`, `relation`, and `target.identifier`, in that order, using the `mapping` namespace (see [Identifier syntax](/standard/identifier-syntax/#mappingassertion-seed)). It MUST remain UUID-based and MUST NOT be derived from provider URLs, corpus paths, or resolver structures. Resolver-target entries do not have their own identifiers.
@@ -324,23 +317,18 @@ This is the case that motivates separating identity from location. The New Testa
"key": "bible-book-chapter-verse",
"type": "CitationSystem",
"preferred_label": "Bible book-chapter-verse (OSIS-style)",
- "normalization_version": "1.0.0",
+ "description": "OSIS-style locator: `Book.Chapter.Verse`. The book component accepts OSIS-compatible book identifiers (letters, digits, and underscores, starting with a letter; no spaces). Both the Hebrew Bible and the New Testament cite with this grammar.",
"locator_regex": "^(?[A-Za-z][A-Za-z0-9_]*)\\.(?[1-9][0-9]*)\\.(?[1-9][0-9]*)$",
- "examples": {
- "valid": ["Genesis.1.1", "Psalms.23.1", "Matthew.5.3"],
- "invalid": ["Genesis.0.1", "Genesis.1", "1.1.1", "Genesis 1:1"]
- },
"status": "active",
"created": "2026-05-31",
"modified": "2026-05-31"
},
{
- "id": "https://textrefs.org/id/ref/59a2d83f-6aff-5fbf-b8f7-b243c3ed0594",
+ "id": "https://textrefs.org/id/ref/b6438d55-f3f2-5fc7-ab40-4f582f8774c3",
"type": "CanonicalReference",
"work_key": "new-testament",
"citation_system_key": "bible-book-chapter-verse",
"locator": "John.3.16",
- "normalization_version": "1.0.0",
"resolver_targets": [
{
"url": "https://www.stepbible.org/?q=version=SBLG|reference=John.3.16",
@@ -379,8 +367,8 @@ A conforming validator MUST check:
2. object `type` values and TextRefs URI patterns, including `Work` and `CitationSystem` IDs whose keys occupy exactly one path segment;
3. flat-key syntax and uniqueness for `Work.key` and `CitationSystem.key`;
4. administrative metadata and `status` values;
-5. citation-system `locator_regex` syntax, and its valid/invalid examples;
-6. canonical-reference locator syntax (the `normalization_version` is the value fixed at minting, verified by the deterministic identifier in item 8, not matched against the system's current version);
+5. citation-system `locator_regex` syntax;
+6. canonical-reference locator syntax: the locator MUST be the profile's canonical spelling and match `locator_regex`; non-canonical spellings MUST be rejected, not normalized;
7. canonical-reference semantic validity: accepted records must be registered, attested reference points for their `Work` and `CitationSystem`;
8. deterministic-identifier correctness for canonical references and mapping assertions;
9. UUID-based identifier shape for `CanonicalReference` and `MappingAssertion` records;
diff --git a/src/content/docs/standard/system-profiles.md b/src/content/docs/standard/system-profiles.md
index 4abeeb5..4436cf1 100644
--- a/src/content/docs/standard/system-profiles.md
+++ b/src/content/docs/standard/system-profiles.md
@@ -6,7 +6,7 @@ sidebar:
order: 4
---
-Citation-system profiles constrain locator syntax through regular expressions and, where needed, additional documented validation rules. A pull request that adds or changes a citation system MUST include the profile record, examples of valid locators, and examples of invalid locators.
+Citation-system profiles constrain locator syntax through regular expressions and, where needed, additional documented validation rules. A pull request that adds or changes a citation system MUST include the full profile record.
## Required profile fields
@@ -14,14 +14,16 @@ Citation-system profiles constrain locator syntax through regular expressions an
- `key`: flat stable key used for deterministic UUID seeds.
- `type`: `CitationSystem`.
- `preferred_label`: human-readable label.
-- `normalization_version`: SemVer version.
+- `description`: prose description of the citation tradition and its canonical locator form.
- `locator_regex`: ECMAScript regular expression for machine-checkable locator pre-validation.
-- `examples.valid`: locator examples that MUST match.
-- `examples.invalid`: locator examples that MUST NOT match.
See [Specification §7](/standard/specification/#7-citationsystem) for the full normative field list.
-Profiles MUST follow the flat key and locator Unicode rules in [Identifier syntax](/standard/identifier-syntax/). A profile MAY add stricter locator rules for case, digits, punctuation, whitespace, allowed scripts, or non-regex-checkable constraints. Regex-checkable constraints MUST be reflected in examples and `locator_regex`; other constraints MUST be documented in the profile. The machine-actionable contract is the flat key, `normalization_version`, `locator_regex`, and examples.
+## Canonical locator form
+
+Every profile defines exactly one canonical spelling for each reference point. Profiles MUST follow the flat key and locator Unicode rules in [Identifier syntax](/standard/identifier-syntax/). A profile MAY add stricter locator rules for case, digits, punctuation, whitespace, allowed scripts, or non-regex-checkable constraints. Regex-checkable constraints — including canonical ASCII digit forms (leading zeros) and letter case — SHOULD be encoded in `locator_regex`; other constraints MUST be documented in `description`. The machine-actionable contract is the flat key and `locator_regex`.
+
+Because the canonical locator seeds the deterministic reference UUID, alternative spellings of the same reference point (`John.3.16` vs `john.3.16`, `514a1` vs `514a01`) would mint distinct permanent identities. Validators MUST reject non-canonical spellings; they MUST NOT fold them into the canonical form. The enumerated reference data in the registry is the authority on which locators are canonical and attested; `locator_regex` is the machine-checkable floor beneath it.
## Seed profiles
@@ -30,12 +32,8 @@ Bekker profile (Aristotelian corpus). Implements **Bekker numbering**, the page-
```json
{
"key": "bekker",
- "normalization_version": "1.0.0",
- "locator_regex": "^[0-9]{3,4}[ab][0-9]{1,2}$",
- "examples": {
- "valid": ["983b10", "1003a21"],
- "invalid": ["983", "983c10", "983b"]
- }
+ "description": "Bekker numbering: page, column (a or b), and line, after August Immanuel Bekker's 1831 Berlin edition. Pages span the whole corpus, roughly 1–1462.",
+ "locator_regex": "^[0-9]{3,4}[ab][0-9]{1,2}$"
}
```
@@ -44,15 +42,11 @@ Stephanus profile (Platonic corpus). Implements **Stephanus pagination**, the pa
```json
{
"key": "stephanus",
- "normalization_version": "1.0.0",
- "locator_regex": "^[0-9]{1,4}[a-e](?:[0-9]{1,2})?$",
- "examples": {
- "valid": ["514a", "514a1"],
- "invalid": ["514f", "514", "514a100"]
- }
+ "description": "Stephanus pagination: page, section (a–e), and optional sub-line, after Henri Estienne's 1578 edition of Plato's works.",
+ "locator_regex": "^[0-9]{1,4}[a-e](?:[0-9]{1,2})?$"
}
```
## Validation rule
-Every `CanonicalReference` MUST point to a known `CitationSystem`. Its `locator` MUST match that system's `locator_regex`, and its `normalization_version` MUST be the value fixed when the reference was minted (see [Specification §8](/standard/specification/#8-canonicalreference)); it need not equal the system's current `normalization_version`. Regex success is necessary but not sufficient: a usable TextRefs reference must resolve to a registered `CanonicalReference` and satisfy any additional profile validation rules.
+Every `CanonicalReference` MUST point to a known `CitationSystem`. Its `locator` MUST be the profile's canonical spelling and MUST match that system's `locator_regex` (see [Specification §8](/standard/specification/#8-canonicalreference)). Regex success is necessary but not sufficient: a usable TextRefs reference must resolve to a registered `CanonicalReference` and satisfy any additional profile validation rules.
diff --git a/src/lib/registry.fixture.ts b/src/lib/registry.fixture.ts
index d903298..1495473 100644
--- a/src/lib/registry.fixture.ts
+++ b/src/lib/registry.fixture.ts
@@ -22,9 +22,8 @@ export const fixtureRegistry: CompiledRegistry = {
key: 'fixture-section',
type: 'CitationSystem',
preferred_label: 'Fixture section',
- normalization_version: '1.0.0',
+ description: 'Single positive integer section number.',
locator_regex: '^(?\\d+)$',
- examples: { valid: ['1'], invalid: ['a'] },
status: 'candidate',
created: '2026-01-01',
modified: '2026-01-01',
@@ -37,7 +36,6 @@ export const fixtureRegistry: CompiledRegistry = {
work_key: 'fixture.work',
citation_system_key: 'fixture-section',
locator: '1',
- normalization_version: '1.0.0',
resolver_targets: [
{
url: 'https://example.org/fixture-work/1',
diff --git a/src/pages/id/ref/[uuid]/index.astro b/src/pages/id/ref/[uuid]/index.astro
index 001ced4..c82771f 100644
--- a/src/pages/id/ref/[uuid]/index.astro
+++ b/src/pages/id/ref/[uuid]/index.astro
@@ -103,7 +103,6 @@ const jsonHref = `/id/ref/${uuidOf(ref.id)}.json`;
diff --git a/standard/schema/canonical-reference.ts b/standard/schema/canonical-reference.ts
index 42cba54..9bc8f54 100644
--- a/standard/schema/canonical-reference.ts
+++ b/standard/schema/canonical-reference.ts
@@ -1,5 +1,5 @@
import { z } from 'zod';
-import { AdminMetadata, FlatKey, Iri, IsoDate, SemVer } from './common.js';
+import { AdminMetadata, FlatKey, Iri, IsoDate } from './common.js';
export const ResolverTargetEntry = z.object({
url: Iri,
@@ -24,7 +24,6 @@ export const CanonicalReferenceBase = AdminMetadata.extend({
work_key: FlatKey,
citation_system_key: FlatKey,
locator: z.string().min(1),
- normalization_version: SemVer,
resolver_targets: z.array(ResolverTargetEntry).default([]),
});
diff --git a/standard/schema/citation-system.ts b/standard/schema/citation-system.ts
index 1dcfad0..39acb3e 100644
--- a/standard/schema/citation-system.ts
+++ b/standard/schema/citation-system.ts
@@ -1,17 +1,13 @@
import { z } from 'zod';
-import { AdminMetadata, FlatKey, SemVer } from './common.js';
+import { AdminMetadata, FlatKey } from './common.js';
export const CitationSystemBase = AdminMetadata.extend({
id: z.string().regex(/^https:\/\/textrefs\.org\/id\/system\/[^/]+$/),
key: FlatKey,
type: z.literal('CitationSystem'),
preferred_label: z.string().min(1),
- normalization_version: SemVer,
+ description: z.string().min(1),
locator_regex: z.string().min(1),
- examples: z.object({
- valid: z.array(z.string()).min(1),
- invalid: z.array(z.string()).min(1),
- }),
});
export const CitationSystem = CitationSystemBase.superRefine((s, ctx) => {
@@ -22,33 +18,15 @@ export const CitationSystem = CitationSystemBase.superRefine((s, ctx) => {
path: ['id'],
});
}
- let re: RegExp;
try {
- re = new RegExp(s.locator_regex);
+ new RegExp(s.locator_regex);
} catch {
ctx.addIssue({
code: 'custom',
message: 'locator_regex is not a valid ECMAScript regex',
path: ['locator_regex'],
});
- return;
}
- s.examples.valid.forEach((v, i) => {
- if (!re.test(v))
- ctx.addIssue({
- code: 'custom',
- message: `examples.valid[${i}] does not match locator_regex`,
- path: ['examples', 'valid', i],
- });
- });
- s.examples.invalid.forEach((v, i) => {
- if (re.test(v))
- ctx.addIssue({
- code: 'custom',
- message: `examples.invalid[${i}] must NOT match locator_regex`,
- path: ['examples', 'invalid', i],
- });
- });
});
export type CitationSystem = z.infer;
diff --git a/standard/schema/common.ts b/standard/schema/common.ts
index fa06fd6..b4d83cf 100644
--- a/standard/schema/common.ts
+++ b/standard/schema/common.ts
@@ -23,7 +23,3 @@ export const AdminMetadata = z.object({
export const FlatKey = z
.string()
.regex(/^[a-z0-9][a-z0-9._-]*$/, 'flat key syntax: ^[a-z0-9][a-z0-9._-]*$');
-
-export const SemVer = z
- .string()
- .regex(/^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/);
From 82bc89982219d6081b7d0e497aae48f25f7268f6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
Date: Sun, 5 Jul 2026 21:25:01 +0200
Subject: [PATCH 14/24] feat(spec): draft lifecycle with retractable
pre-promotion identity (ADR-0003) (#23)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat(spec): add draft lifecycle with retractable pre-promotion identity
Adopt ADR-0003 (fixes #22): new draft status is an ephemeral tier below
candidate. The identifier-persistence promise attaches at the promotion
event (draft -> candidate, expert review), not at publication. Draft
records may be corrected (identity change mints a different id) or
retracted (deleted, no tombstone); a re-proposed tuple regains the same
UUID by construction. All records keep deterministic v5 ids at every
stage - governance §6.3/6.4 no longer prescribes generated ids for
provisional objects.
- specification §11/§12: promotion semantics + draft status definition
- identifier-syntax: immutability scoped to candidate-or-higher
- versioning: ladder, draft-retraction section, promoted-must-not-cite-
draft invariant (enforcement is an ADR follow-up)
- governance EN/DE §4-§6: entry via technical review, promotion via
expert review, tombstones scoped to promoted records
- CONTRIBUTING: review tracks + flowchart
- Status enum: draft added (Zod + OpenAPI)
Co-Authored-By: Claude Fable 5
* chore(data): bump submodule to draft-demoted registry (3f6c9f2)
All records enter the ADR-0003 ladder at draft; see textrefs/registry#7.
Co-Authored-By: Claude Fable 5
---------
Co-authored-by: Moritz Mähr <14755525+maehr@users.noreply.github.com>
Co-authored-by: Claude Fable 5
---
CONTRIBUTING.md | 10 +--
api/openapi.yaml | 2 +-
data | 2 +-
...0003-draft-lifecycle-ephemeral-identity.md | 72 +++++++++++++++++++
src/content/docs/association/governance.md | 12 ++--
src/content/docs/de/association/governance.md | 12 ++--
.../docs/standard/identifier-syntax.md | 4 +-
src/content/docs/standard/specification.md | 7 +-
src/content/docs/standard/versioning.md | 12 +++-
standard/schema/common.ts | 1 +
10 files changed, 115 insertions(+), 19 deletions(-)
create mode 100644 decisions/ADR-0003-draft-lifecycle-ephemeral-identity.md
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2357b80..8a33029 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -28,19 +28,19 @@ A contribution does not create a claim to acceptance, prioritization, publicatio
Changes are routed to one of three tracks:
-- **Technical review** — typos, formatting, broken links, minor metadata, `last_checked` updates, uncontested aliases, build / tooling fixes. Needs automated validation and one technical reviewer.
-- **Expert review** — new works, new citation systems, new corpora, contested mappings, changes to deterministic ID inputs, status changes (`active` / `deprecated` / `withdrawn` / `blocked`). Needs technical validation, a documented rationale with sources, and at least one expert reviewer.
+- **Technical review** — typos, formatting, broken links, minor metadata, `last_checked` updates, uncontested aliases, build / tooling fixes, and merging new registry data as `draft`. Needs automated validation and one technical reviewer.
+- **Expert review** — new works, new citation systems, new corpora, contested mappings, changes to deterministic ID inputs, status changes (promotion `draft` → `candidate`, and `active` / `deprecated` / `withdrawn` / `blocked`). Needs technical validation, a documented rationale with sources, and at least one expert reviewer.
- **Board reservation** — takedowns, blocking, licence policy, and other legal or policy-sensitive matters. Decided by the Association Board.
```mermaid
flowchart TD
S["Contribution (issue or PR)"] --> T{Triage}
T -->|"typos, formatting, links, metadata, tooling"| TR[Technical review]
- T -->|"new work / system / corpus, contested mapping, ID inputs, status change"| ER[Expert review]
+ T -->|"new work / system / corpus, contested mapping, ID inputs, status change / promotion"| ER[Expert review]
T -->|"takedown, blocking, licence / policy"| BR[Board reservation]
TR --> V{"Automated validation + 1 technical reviewer"}
ER --> V2{"Validation + rationale and sources + 1 expert reviewer"}
- V -->|pass| A([Accepted / merged])
+ V -->|pass| A(["Accepted / merged (new data lands as draft)"])
V -->|fail| R([Rejected, with reason])
V2 -->|pass| A
V2 -->|fail| R
@@ -48,6 +48,8 @@ flowchart TD
BR -->|decision| R
```
+New registry records enter at `status: draft` after technical review; they stay retractable until an expert review promotes them to `candidate`, which permanently freezes their identifier (see the [versioning rules](https://textrefs.org/standard/versioning/) and governance §5).
+
## Local development
Prerequisites: Node 24 and npm.
diff --git a/api/openapi.yaml b/api/openapi.yaml
index 37abd41..6b951c0 100644
--- a/api/openapi.yaml
+++ b/api/openapi.yaml
@@ -182,7 +182,7 @@ components:
pattern: '^\d{4}-\d{2}-\d{2}$'
Status:
type: string
- enum: [candidate, active, deprecated, withdrawn, blocked]
+ enum: [draft, candidate, active, deprecated, withdrawn, blocked]
AdminMetadata:
type: object
required: [status, created, modified]
diff --git a/data b/data
index 5337e71..3f6c9f2 160000
--- a/data
+++ b/data
@@ -1 +1 @@
-Subproject commit 5337e712f42b510ae2515549080b3cbb60aec254
+Subproject commit 3f6c9f26a158cfeb74a32a819ebc5f856f2be984
diff --git a/decisions/ADR-0003-draft-lifecycle-ephemeral-identity.md b/decisions/ADR-0003-draft-lifecycle-ephemeral-identity.md
new file mode 100644
index 0000000..06e4b29
--- /dev/null
+++ b/decisions/ADR-0003-draft-lifecycle-ephemeral-identity.md
@@ -0,0 +1,72 @@
+# ADR-0003: Review lifecycle with an ephemeral `draft` state
+
+- **Status:** Proposed
+- **Date:** 2026-07-05
+- **Deciders:** @maehr
+- **Tags:** spec, governance
+
+## Context and problem statement
+
+Every published record carries the full persistence promise: IRIs are permanent once minted (identifier-syntax § Immutability, specification §11), and any identity correction requires tombstone ceremony (versioning § Tombstones). There is no state in which a record is visible in the registry but still retractable — a wrong locator cannot simply be fixed, a bad entry cannot simply vanish. Issue [#22](https://github.com/textrefs/textrefs.org/issues/22) proposes such a state.
+
+The governance regulation defines review _tracks_ (§4: technical / expert / board) but no in-registry review _lifecycle_: nothing distinguishes freshly imported, unreviewed data from records that have passed expert review.
+
+Since ADR-0002, identity is pure math: the reference UUID is offline-computable from `(work_key, citation_system_key, locator)`. That separates **identity** (which never mutates — a tuple always hashes to the same UUID) from **registration** (what the registry endorses and promises). It also puts governance §6.4 ("provisional or uncertain objects receive generated IDs") in conflict with the deterministic-identity model.
+
+## Decision drivers
+
+- Reviewers need to browse rendered records and check resolver targets _before_ the registry commits to permanence.
+- Corrections during review should be cheap: fix the tuple, the wrong UUID disappears — no tombstone debt for data nobody ever relied on.
+- The persistence promise must stay absolute where it applies; a promise with fuzzy edges is worse than a narrower hard one.
+- ADR-0002's offline computability must hold at every lifecycle stage; re-minting ceremonies contradict it.
+- The DE governance regulation is the legally binding text; the model must be expressible there in sync.
+
+## Considered options
+
+1. **Attach the promise at `candidate → active`** — no new status; `candidate` becomes retractable. Silently weakens what `candidate` means today and leaves no distinction between "just imported" and "proposed for acceptance".
+2. **Never publish ephemeral records (PR-only review)** — everything published stays permanent. Reviewers cannot browse rendered records; long-running review branches become the de-facto ephemeral state.
+3. **v4 provisional IDs, re-minted to v5 at promotion** (governance §6.4 as written) — ephemerality visible in the ID itself, but breaks offline computability for drafts and makes every promotion an ID migration.
+4. **New `draft` status; promise attaches at `draft → candidate` promotion; deterministic v5 throughout.**
+
+## Decision
+
+We choose **Option 4**. The status ladder becomes `draft` → `candidate` → `active` → `deprecated` / `withdrawn` / `blocked`, and the identifier-persistence promise attaches at the **promotion** event (`draft → candidate`), not at publication.
+
+**Entry.** New data enters the registry as `status: draft` after technical review (governance §4.1: schema validation, `locator_regex`, no full text). Draft records are rendered with a prominent "draft — not a persistent identifier" treatment (noindex, excluded from sitemap and search) and appear in exports with `status` as the signal — the same convention tombstones use.
+
+**Ephemerality.** Draft records are excluded from the persistence policy. They MAY be corrected — changing an identity field mints a different id; the old UUID simply disappears — or retracted: the record is deleted, its IRI ceases to resolve, and **no tombstone** is created. Because identity is deterministic, a retracted tuple that is later re-proposed regains the same UUID by construction; there is no identity split and no way to "lose" an identity by retraction.
+
+**Promotion.** `draft → candidate` requires expert review (governance §4.2: rationale, sources, attestation of the reference points, canonical-form check). Promotion changes `status` only — it MUST NOT change identity fields, so the IRI survives promotion unchanged. From promotion onward the record is permanent and the tombstone rules of versioning apply. `candidate → active` and the tombstone states are unchanged.
+
+**Granularity.** References share their work's status (the compiler assigns `status` from the work record), so the promotion unit is a work together with its references; citation systems and mappings promote individually. A promoted record MUST NOT reference a draft `Work` or `CitationSystem` through its keys — systems and works are promoted before or together with the references that depend on them.
+
+**ID policy.** All registry records carry deterministic v5 identifiers at every stage. Provisionality is expressed by `draft` status, not by the ID algorithm; governance §6.3/6.4 is amended accordingly.
+
+## Consequences
+
+### Positive
+
+- The registry gains a reviewable, retractable tier without weakening any existing promise: permanence attaches at promotion and is absolute from there.
+- Review-stage corrections cost nothing: no tombstone debt for data nobody ever relied on.
+- One lifecycle connects the governance review tracks (§4) to the status model (§5): technical review gates entry, expert review gates permanence.
+- The earlier wish to downgrade the unreviewed seed data below `candidate` becomes meaningful and lands as a follow-up.
+
+### Negative / trade-offs
+
+- Dump consumers MUST filter by `status` if they need only promoted records; a draft's presence in one release says nothing about the next.
+- A retracted draft's IRI returns 404 rather than a tombstone page — link rot is possible for anyone who cited a draft despite the flagging.
+- Re-proposal reuses the same UUID; consumers must not treat a reappearing id as continuity of curation history.
+- Two texts (EN + legally binding DE governance) must be amended in sync.
+
+### Follow-up actions
+
+- [ ] Compiler invariant: a promoted (`candidate`+) record MUST NOT reference a draft `Work`/`CitationSystem` (extend the tombstone invariant in `scripts/compile.ts`).
+- [ ] Draft rendering: banner, `noindex`, sitemap/search exclusion on record pages (extend the `Tombstone.astro` pattern).
+- [ ] Downgrade the current unreviewed seed data to `draft` in `textrefs/registry`.
+- [ ] Optional CI persistence check: promoted ids diffed against the previous release dump.
+
+## Links
+
+- Related ADRs: ADR-0002 (deterministic identity from the semantic tuple — the identity/registration separation this builds on)
+- Related issues / PRs: textrefs/textrefs.org#22, textrefs/textrefs.org#15
+- External references: governance regulation §4–§6 (`/association/governance/`)
diff --git a/src/content/docs/association/governance.md b/src/content/docs/association/governance.md
index fe0c291..7d41842 100644
--- a/src/content/docs/association/governance.md
+++ b/src/content/docs/association/governance.md
@@ -96,7 +96,8 @@ A technical review is usually sufficient for:
- minor metadata corrections;
- updates to `last_checked` fields;
- adding uncontested aliases;
-- documentation corrections without expert implications.
+- documentation corrections without expert implications;
+- entry of new records as `draft` (merging validated data into the registry).
Prerequisites:
@@ -118,6 +119,7 @@ An expert review is required for:
- contested mapping statements;
- conflicting external identifiers;
- changes with significant expert or reputational impact;
+- promotion of records from `draft` to `candidate` (the point at which the persistence guarantee attaches);
- status changes to `active`, `deprecated`, `withdrawn`, or `blocked`, where these are subject-matter decisions.
Prerequisites:
@@ -143,6 +145,7 @@ The Board decides or confirms:
### 5.1 General record status
+- **draft:** work-in-progress record; correctable or retractable without a tombstone; excluded from the persistence guarantee;
- **candidate:** proposed record, not yet fully reviewed;
- **active:** valid and recommended;
- **deprecated:** no longer recommended, but retained for historical reasons;
@@ -151,6 +154,7 @@ The Board decides or confirms:
### 5.2 Mapping status
+- **draft:** work-in-progress mapping; correctable or retractable without a tombstone;
- **candidate:** proposed mapping;
- **active:** reviewed and recommended mapping;
- **deprecated:** mapping no longer recommended;
@@ -161,7 +165,7 @@ Contested mappings remain in the appropriate schema status and carry the documen
### 5.3 Tombstone principle
-Published IDs are, as a matter of principle, not hard-deleted.
+Promoted IDs (status `candidate` or higher) are, as a matter of principle, not hard-deleted. Records in `draft` status may be retracted without a landing page.
In the event of withdrawal, blocking, or deprecation, a landing page is retained with:
@@ -176,8 +180,8 @@ In the event of withdrawal, blocking, or deprecation, a landing page is retained
1. Primary TextRefs IDs are independent HTTP URIs.
2. External identifiers such as CTS URNs, Wikidata IDs, DOIs, ARKs, Perseus URLs, or Scaife URLs are not primary TextRefs IDs.
-3. Stable and established references may receive deterministic IDs.
-4. Provisional or uncertain objects receive generated IDs.
+3. All registry records carry deterministic IDs computable from their identity fields.
+4. Provisional records are expressed through the `draft` status, which is excluded from the persistence guarantee; their IDs may disappear or change until promotion.
5. Once published, IDs are not changed merely because labels, titles, aliases, or external mappings are improved.
6. Human-readable citation URLs are aliases and may be redirected, changed, or deprecated; primary IDs remain authoritative.
diff --git a/src/content/docs/de/association/governance.md b/src/content/docs/de/association/governance.md
index 31fded2..118b764 100644
--- a/src/content/docs/de/association/governance.md
+++ b/src/content/docs/de/association/governance.md
@@ -96,7 +96,8 @@ Ein technischer Review genügt in der Regel für:
- kleinere Metadatenkorrekturen;
- Aktualisierung von `last_checked`-Feldern;
- Ergänzung unstrittiger Aliase;
-- Dokumentationskorrekturen ohne fachliche Tragweite.
+- Dokumentationskorrekturen ohne fachliche Tragweite;
+- Aufnahme neuer Datensätze als `draft` (Merge validierter Daten in das Register).
Voraussetzungen:
@@ -118,6 +119,7 @@ Ein fachlicher Review ist erforderlich für:
- strittige Mapping-Aussagen;
- widersprüchliche externe Identifikatoren;
- Änderungen mit erheblicher fachlicher oder reputationsbezogener Wirkung;
+- Promotion von Datensätzen von `draft` zu `candidate` (der Zeitpunkt, an dem die Persistenzzusage beginnt);
- Statusänderungen zu `active`, `deprecated`, `withdrawn` oder `blocked`, soweit fachlich begründet.
Voraussetzungen:
@@ -143,6 +145,7 @@ Der Vorstand entscheidet oder bestätigt:
### 5.1 Allgemeine Datensatzstatus
+- **draft:** Datensatz in Arbeit; ohne Tombstone korrigierbar oder zurückziehbar; von der Persistenzzusage ausgenommen;
- **candidate:** vorgeschlagener Datensatz, noch nicht vollständig geprüft;
- **active:** gültig und empfohlen;
- **deprecated:** nicht mehr empfohlen, aber aus historischen Gründen erhalten;
@@ -151,6 +154,7 @@ Der Vorstand entscheidet oder bestätigt:
### 5.2 Mapping-Status
+- **draft:** Zuordnung in Arbeit; ohne Tombstone korrigierbar oder zurückziehbar;
- **candidate:** vorgeschlagene Zuordnung;
- **active:** geprüfte und empfohlene Zuordnung;
- **deprecated:** nicht mehr empfohlene Zuordnung;
@@ -161,7 +165,7 @@ Strittige Mappings behalten den passenden Schema-Status und führen Begründung,
### 5.3 Tombstone-Prinzip
-Publizierte IDs werden grundsätzlich nicht hart gelöscht.
+Promovierte IDs (Status `candidate` oder höher) werden grundsätzlich nicht hart gelöscht. Datensätze im Status `draft` können ohne Landing Page zurückgezogen werden.
Bei Rückzug, Sperrung oder Deprecation bleibt eine Landing Page erhalten mit:
@@ -176,8 +180,8 @@ Bei Rückzug, Sperrung oder Deprecation bleibt eine Landing Page erhalten mit:
1. Primäre TextRefs-IDs sind unabhängige HTTP-URIs.
2. Externe Identifikatoren wie CTS URNs, Wikidata-IDs, DOIs, ARKs, Perseus-URLs oder Scaife-URLs sind keine primären TextRefs-IDs.
-3. Stabile und etablierte Referenzen können deterministische IDs erhalten.
-4. Provisorische oder unsichere Objekte erhalten generierte IDs.
+3. Alle Registereinträge tragen deterministische IDs, die aus ihren Identitätsfeldern berechenbar sind.
+4. Provisorische Einträge werden über den Status `draft` ausgedrückt, der von der Persistenzzusage ausgenommen ist; ihre IDs können bis zur Promotion verschwinden oder sich ändern.
5. Einmal publizierte IDs werden nicht geändert, nur weil Labels, Titel, Aliase oder externe Mappings verbessert werden.
6. Lesbare Citation-URLs sind Aliase und können umgeleitet, geändert oder deprecated werden; primäre IDs bleiben massgeblich.
diff --git a/src/content/docs/standard/identifier-syntax.md b/src/content/docs/standard/identifier-syntax.md
index 3c0dd8b..1310779 100644
--- a/src/content/docs/standard/identifier-syntax.md
+++ b/src/content/docs/standard/identifier-syntax.md
@@ -125,6 +125,8 @@ The canonical URI is `https://textrefs.org/id/mapping/{uuid}`.
## Immutability
-Once a deterministic identifier is published, it is permanent. If a record is found to be wrong, it MUST be marked `deprecated`, `withdrawn`, or `blocked`; the original URI MUST remain dereferenceable as a tombstone.
+Once a deterministic identifier is published at status `candidate` or higher, it is permanent. If a promoted record is found to be wrong, it MUST be marked `deprecated`, `withdrawn`, or `blocked`; the original URI MUST remain dereferenceable as a tombstone.
+
+Records at status `draft` are exempt: they MAY be corrected or retracted without a tombstone, and a retracted draft IRI simply ceases to resolve (see [Specification §11](/standard/specification/#11-identifier-policy) and [Versioning](/standard/versioning/#draft-records-and-retraction)). Because identity is deterministic, a retracted tuple that is later re-proposed regains the same UUID by construction.
See [Specification §11](/standard/specification/#11-identifier-policy) for the normative identifier policy.
diff --git a/src/content/docs/standard/specification.md b/src/content/docs/standard/specification.md
index c432725..d9d5988 100644
--- a/src/content/docs/standard/specification.md
+++ b/src/content/docs/standard/specification.md
@@ -267,7 +267,9 @@ A `CanonicalReference` identifier MUST be generated deterministically. The ident
A `MappingAssertion` identifier MUST be generated deterministically from `subject`, `relation`, and `target.identifier`, in that order, using the `mapping` namespace (see [Identifier syntax](/standard/identifier-syntax/#mappingassertion-seed)). It MUST remain UUID-based and MUST NOT be derived from provider URLs, corpus paths, or resolver structures. Resolver-target entries do not have their own identifiers.
-An implementation MUST NOT silently change the identity-defining fields of an existing `CanonicalReference`. Because those fields seed the deterministic identifier, any change produces a new `CanonicalReference` with a new identifier. The prior reference MUST be retained as a tombstone (`status` `deprecated` or `withdrawn`, [§12](#12-administrative-metadata)) and SHOULD be linked to its replacement through an `exactMatch` `MappingAssertion` ([§10](#10-mappingassertion)).
+The persistence promise attaches at **promotion**: the first time a record is published at status `candidate` or higher ([§12](#12-administrative-metadata)). Promotion changes `status` only and MUST NOT change identity-defining fields, so the identifier survives promotion unchanged. Records at status `draft` are excluded from the persistence policy: they MAY be corrected (changing an identity field mints a different identifier; the previous one ceases to resolve) or retracted (the record is deleted) without a tombstone.
+
+An implementation MUST NOT silently change the identity-defining fields of an existing **promoted** `CanonicalReference`. Because those fields seed the deterministic identifier, any change produces a new `CanonicalReference` with a new identifier. The prior reference MUST be retained as a tombstone (`status` `deprecated` or `withdrawn`, [§12](#12-administrative-metadata)) and SHOULD be linked to its replacement through an `exactMatch` `MappingAssertion` ([§10](#10-mappingassertion)).
A conforming registry SHOULD publish each `/id/{type}/{key}` IRI at two static URLs: the canonical URL itself (HTML for browsers) and a sibling with a `.json` extension carrying the JSON-LD payload. The HTML representation SHOULD advertise the JSON-LD sibling via `` in the document head. `Accept`-header content negotiation is not required.
@@ -285,7 +287,8 @@ Every registry object MUST include:
- `created` and `modified` MUST be [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) calendar dates in `YYYY-MM-DD` form.
- `status` MUST be one of:
- - `candidate` — proposed but not yet accepted as stable.
+ - `draft` — work-in-progress record under review; **excluded from the identifier persistence policy** ([§11](#11-identifier-policy)). May be corrected or retracted without a tombstone.
+ - `candidate` — proposed but not yet accepted as stable. Promotion from `draft` attaches the persistence promise.
- `active` — accepted and recommended for use.
- `deprecated` — retained but no longer recommended.
- `withdrawn` — removed from active use because it was erroneous or has been superseded. If a successor exists, it is linked by an `exactMatch` `MappingAssertion`; see [Versioning](/standard/versioning/) for tombstones.
diff --git a/src/content/docs/standard/versioning.md b/src/content/docs/standard/versioning.md
index 146514e..0c59a5e 100644
--- a/src/content/docs/standard/versioning.md
+++ b/src/content/docs/standard/versioning.md
@@ -72,11 +72,17 @@ Each `datapackage.json` MUST include:
## Per-record versioning
-Records do **not** carry their own SemVer. The registry is append-only with status transitions (`candidate` → `active` → `deprecated` / `withdrawn` / `blocked`). Consumers pin to a registry tag (or its DOI) for reproducibility. Identifier-level changes are expressed via tombstones, below.
+Records do **not** carry their own SemVer. The registry is append-only from promotion onward, with status transitions (`draft` → `candidate` → `active` → `deprecated` / `withdrawn` / `blocked`). The `draft` tier is pre-persistence: see [Draft records and retraction](#draft-records-and-retraction). Consumers pin to a registry tag (or its DOI) for reproducibility. Identifier-level changes to promoted records are expressed via tombstones, below.
+
+## Draft records and retraction
+
+Records at status `draft` have not been promoted and carry no persistence promise ([Specification §11](/standard/specification/#11-identifier-policy)). A draft MAY be corrected — changing an identity field mints a different id, and the previous IRI ceases to resolve — or retracted, meaning the record is deleted outright. Retraction MUST NOT create a tombstone. Because identifiers are deterministic, a retracted tuple that is later re-proposed regains the same UUID; a reappearing id does not imply continuity of curation history.
+
+Draft records appear in exports inside the same `.jsonl` files as their type, with `status` as the signal — the same convention tombstones use. Consumers MUST NOT rely on a draft record persisting across releases and SHOULD filter on `status` when they need only promoted records. Rendered draft pages SHOULD be clearly flagged and excluded from search indexing.
## Tombstones and re-minted records
-Registry identity is permanent: the IRI of a `Work`, `CitationSystem`, `CanonicalReference`, or `MappingAssertion` MUST continue to resolve once minted. Re-minting (renaming a key, correcting a locator that changes the content-derived UUID, splitting/merging records) MUST be expressed by **tombstoning** the old record and minting a successor.
+Registry identity is permanent once promoted: the IRI of a `Work`, `CitationSystem`, `CanonicalReference`, or `MappingAssertion` MUST continue to resolve once the record has been published at status `candidate` or higher. Re-minting a promoted record (renaming a key, correcting a locator that changes the content-derived UUID, splitting/merging records) MUST be expressed by **tombstoning** the old record and minting a successor.
### Schema
@@ -98,6 +104,8 @@ Tombstones MUST appear in monthly exports inside the same `.jsonl` file as their
The compiler enforces: an active `CanonicalReference` MUST NOT reference a tombstoned `Work` or `CitationSystem` through `work_key` or `citation_system_key`. `MappingAssertion`s are exempt — successor links from a withdrawn subject to an active target are exactly the documented pattern.
+Analogously, a promoted (`candidate` or higher) record MUST NOT reference a `draft` `Work` or `CitationSystem` through its keys: systems and works are promoted before or together with the records that depend on them.
+
### Aliases vs. tombstones
`aliases.json` handles **presentational** URL aliases (multiple paths pointing at the same canonical record). Tombstones handle **identity** changes (the record itself is no longer canonical). These are distinct mechanisms and MUST NOT be conflated.
diff --git a/standard/schema/common.ts b/standard/schema/common.ts
index b4d83cf..518aec4 100644
--- a/standard/schema/common.ts
+++ b/standard/schema/common.ts
@@ -1,6 +1,7 @@
import { z } from 'zod';
export const Status = z.enum([
+ 'draft',
'candidate',
'active',
'deprecated',
From 19526508692c19f5d9d43e38b63e63bc3d5c7e85 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
<14755525+maehr@users.noreply.github.com>
Date: Sun, 5 Jul 2026 21:35:59 +0200
Subject: [PATCH 15/24] chore(data): bump submodule to registry main (40af385)
All records demoted to draft per ADR-0003 (textrefs/registry#7); pin
returns to a registry main ancestor.
Co-Authored-By: Claude Fable 5
---
data | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/data b/data
index 3f6c9f2..40af385 160000
--- a/data
+++ b/data
@@ -1 +1 @@
-Subproject commit 3f6c9f26a158cfeb74a32a819ebc5f856f2be984
+Subproject commit 40af38538da3a2314dca80862fc281880be6b75a
From a765770ea25cd576c8d596788a52193a32a84608 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
Date: Sun, 5 Jul 2026 21:54:10 +0200
Subject: [PATCH 16/24] feat(spec): require explicit canonical ASCII digit and
case forms in profiles (#24)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Fixes the spec side of #13: system profiles MUST state their
leading-zero policy (default forbidden) and canonical letter case
(default case-sensitive declared casing). Seed-profile examples and the
worked example follow the registry: Bekker pages from 1 without leading
zeros, Stephanus likewise, Bible vocabulary pinned to case-sensitive
OSIS book abbreviations. Submodule bumped to registry main d31ed0b
(registry#8).
Co-authored-by: Moritz Mähr <14755525+maehr@users.noreply.github.com>
Co-authored-by: Claude Fable 5
---
data | 2 +-
src/content/docs/get-started/authoring.md | 4 ++--
.../get-started/mappings-and-resolver-targets.md | 2 +-
src/content/docs/standard/specification.md | 14 +++++++-------
src/content/docs/standard/system-profiles.md | 15 ++++++++++-----
5 files changed, 21 insertions(+), 16 deletions(-)
diff --git a/data b/data
index 40af385..d31ed0b 160000
--- a/data
+++ b/data
@@ -1 +1 @@
-Subproject commit 40af38538da3a2314dca80862fc281880be6b75a
+Subproject commit d31ed0bb0d561d4e54224b3af1c730e851648491
diff --git a/src/content/docs/get-started/authoring.md b/src/content/docs/get-started/authoring.md
index 71a269d..5e82501 100644
--- a/src/content/docs/get-started/authoring.md
+++ b/src/content/docs/get-started/authoring.md
@@ -198,10 +198,10 @@ references_range:
counts: [16, 24, 26, 26, 28, 30, 38, 21, 31, 27, 26, 24, 30, 47, 42, 14, 26, 11, 25, 5]
# Genesis — 1,533 references from per-chapter verse counts:
-# 'Genesis.1.1', …, 'Genesis.50.26'
+# 'Gen.1.1', …, 'Gen.50.26'
references_range:
- kind: book_chapter_verse
- book: Genesis
+ book: Gen
counts: [31, 25, 24, 26, 32, 22 /* …, 26 */]
# Dhammapada — 423 references from per-chapter verse counts (no book prefix):
diff --git a/src/content/docs/get-started/mappings-and-resolver-targets.md b/src/content/docs/get-started/mappings-and-resolver-targets.md
index 0fbfa3b..eaa3ac3 100644
--- a/src/content/docs/get-started/mappings-and-resolver-targets.md
+++ b/src/content/docs/get-started/mappings-and-resolver-targets.md
@@ -98,7 +98,7 @@ The best TextRefs candidates are works with established, edition-independent cit
| Philosophy | Plato, _Republic_ `514a` | Stephanus pagination | Present as `plato.republic` with `stephanus`. |
| Philosophy | Aristotle, _Nicomachean Ethics_ `1094a1` | Bekker numbering | Present as `aristotle.nicomachean-ethics` with `bekker`. |
| Buddhist | _Dhammapada_ `1.1` | chapter and verse | Present as `dhammapada` with `dhammapada-chapter-verse`. |
-| Tanakh | Genesis `Genesis.1.1` | book, chapter, verse | Present as `tanakh` with `bible-book-chapter-verse`. |
+| Tanakh | Genesis `Gen.1.1` | book, chapter, verse | Present as `tanakh` with `bible-book-chapter-verse`. |
| Bible | John `John.3.16` | book, chapter, verse | Present as `new-testament` with `bible-book-chapter-verse`. |
| Chinese | _Analects_ `2.1` | book and chapter | Present as `confucius.analects` with `analects-book-chapter`. |
| Chinese | _Daodejing_ `1` | chapter | Present as `laozi.daodejing` with `daodejing-chapter`. |
diff --git a/src/content/docs/standard/specification.md b/src/content/docs/standard/specification.md
index d9d5988..f548ad1 100644
--- a/src/content/docs/standard/specification.md
+++ b/src/content/docs/standard/specification.md
@@ -144,9 +144,9 @@ A `CitationSystem.key` is a single flat registry key for a locator notation and
"id": "https://textrefs.org/id/system/bible-book-chapter-verse",
"key": "bible-book-chapter-verse",
"type": "CitationSystem",
- "preferred_label": "Bible book-chapter-verse (OSIS-style)",
- "description": "OSIS-style locator: `Book.Chapter.Verse`. The book component accepts OSIS-compatible book identifiers (letters, digits, and underscores, starting with a letter; no spaces). Both the Hebrew Bible and the New Testament cite with this grammar.",
- "locator_regex": "^(?[A-Za-z][A-Za-z0-9_]*)\\.(?[1-9][0-9]*)\\.(?[1-9][0-9]*)$",
+ "preferred_label": "Bible book-chapter-verse (OSIS)",
+ "description": "OSIS locator: `Book.Chapter.Verse`. The canonical book vocabulary is the OSIS book abbreviation list (CrossWire), case-sensitive as published there — e.g. `Gen`, `Exod`, `Matt`, `John`, `1Cor`. Chapter and verse are positive integers without leading zeros. Both the Hebrew Bible and the New Testament cite with this grammar.",
+ "locator_regex": "^(?[1-4]?[A-Za-z][A-Za-z0-9]*)\\.(?[1-9][0-9]*)\\.(?[1-9][0-9]*)$",
"status": "candidate",
"created": "2026-05-31",
"modified": "2026-05-31"
@@ -298,7 +298,7 @@ Deprecated, withdrawn, and blocked records SHOULD remain visible unless removal
## 13. Worked example: a multi-translation work
-This is the case that motivates separating identity from location. The New Testament exists in many editions and translations, yet `John.3.16` is **one** reference in the OSIS-style book-chapter-verse system.
+This is the case that motivates separating identity from location. The New Testament exists in many editions and translations, yet `John.3.16` is **one** reference in the OSIS book-chapter-verse system.
**One identity** — a single `Work`, `CitationSystem`, and `CanonicalReference`. The reference embeds all language-tagged locations as `resolver_targets`:
@@ -319,9 +319,9 @@ This is the case that motivates separating identity from location. The New Testa
"id": "https://textrefs.org/id/system/bible-book-chapter-verse",
"key": "bible-book-chapter-verse",
"type": "CitationSystem",
- "preferred_label": "Bible book-chapter-verse (OSIS-style)",
- "description": "OSIS-style locator: `Book.Chapter.Verse`. The book component accepts OSIS-compatible book identifiers (letters, digits, and underscores, starting with a letter; no spaces). Both the Hebrew Bible and the New Testament cite with this grammar.",
- "locator_regex": "^(?[A-Za-z][A-Za-z0-9_]*)\\.(?[1-9][0-9]*)\\.(?[1-9][0-9]*)$",
+ "preferred_label": "Bible book-chapter-verse (OSIS)",
+ "description": "OSIS locator: `Book.Chapter.Verse`. The canonical book vocabulary is the OSIS book abbreviation list (CrossWire), case-sensitive as published there — e.g. `Gen`, `Exod`, `Matt`, `John`, `1Cor`. Chapter and verse are positive integers without leading zeros. Both the Hebrew Bible and the New Testament cite with this grammar.",
+ "locator_regex": "^(?[1-4]?[A-Za-z][A-Za-z0-9]*)\\.(?[1-9][0-9]*)\\.(?[1-9][0-9]*)$",
"status": "active",
"created": "2026-05-31",
"modified": "2026-05-31"
diff --git a/src/content/docs/standard/system-profiles.md b/src/content/docs/standard/system-profiles.md
index 4436cf1..2f8717d 100644
--- a/src/content/docs/standard/system-profiles.md
+++ b/src/content/docs/standard/system-profiles.md
@@ -21,7 +21,12 @@ See [Specification §7](/standard/specification/#7-citationsystem) for the full
## Canonical locator form
-Every profile defines exactly one canonical spelling for each reference point. Profiles MUST follow the flat key and locator Unicode rules in [Identifier syntax](/standard/identifier-syntax/). A profile MAY add stricter locator rules for case, digits, punctuation, whitespace, allowed scripts, or non-regex-checkable constraints. Regex-checkable constraints — including canonical ASCII digit forms (leading zeros) and letter case — SHOULD be encoded in `locator_regex`; other constraints MUST be documented in `description`. The machine-actionable contract is the flat key and `locator_regex`.
+Every profile defines exactly one canonical spelling for each reference point. Profiles MUST follow the flat key and locator Unicode rules in [Identifier syntax](/standard/identifier-syntax/). Along two ASCII axes the profile MUST take an explicit position:
+
+1. **Digit sequences.** The profile MUST state whether leading zeros are permitted in numeric components. Unless the citation tradition itself uses them, leading zeros MUST be forbidden.
+2. **Letter case.** The profile MUST declare the canonical case of letter components. Locators are matched case-sensitively against that declared casing; case variants are non-canonical spellings.
+
+A profile MAY add further locator rules for punctuation, whitespace, allowed scripts, or non-regex-checkable constraints (such as a pinned book vocabulary). Regex-checkable constraints — including the two axes above — SHOULD be encoded in `locator_regex`; other constraints MUST be documented in `description`. The machine-actionable contract is the flat key and `locator_regex`.
Because the canonical locator seeds the deterministic reference UUID, alternative spellings of the same reference point (`John.3.16` vs `john.3.16`, `514a1` vs `514a01`) would mint distinct permanent identities. Validators MUST reject non-canonical spellings; they MUST NOT fold them into the canonical form. The enumerated reference data in the registry is the authority on which locators are canonical and attested; `locator_regex` is the machine-checkable floor beneath it.
@@ -32,8 +37,8 @@ Bekker profile (Aristotelian corpus). Implements **Bekker numbering**, the page-
```json
{
"key": "bekker",
- "description": "Bekker numbering: page, column (a or b), and line, after August Immanuel Bekker's 1831 Berlin edition. Pages span the whole corpus, roughly 1–1462.",
- "locator_regex": "^[0-9]{3,4}[ab][0-9]{1,2}$"
+ "description": "Bekker numbering: page, column (a or b), and line, after August Immanuel Bekker's 1831 Berlin edition. Pages span the whole corpus from 1 (Categories 1a1) to roughly 1462. Canonical form: page and line are positive integers without leading zeros; the column letter is lowercase.",
+ "locator_regex": "^[1-9][0-9]{0,3}[ab][1-9][0-9]?$"
}
```
@@ -42,8 +47,8 @@ Stephanus profile (Platonic corpus). Implements **Stephanus pagination**, the pa
```json
{
"key": "stephanus",
- "description": "Stephanus pagination: page, section (a–e), and optional sub-line, after Henri Estienne's 1578 edition of Plato's works.",
- "locator_regex": "^[0-9]{1,4}[a-e](?:[0-9]{1,2})?$"
+ "description": "Stephanus pagination: page, section (a–e), and optional sub-line, after Henri Estienne's 1578 edition of Plato's works. Canonical form: page and sub-line are positive integers without leading zeros; the section letter is lowercase.",
+ "locator_regex": "^[1-9][0-9]{0,3}[a-e](?:[1-9][0-9]?)?$"
}
```
From 37d148114cd91c3c6d1083f2551b5d56eb9f1a19 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
Date: Sun, 5 Jul 2026 22:16:39 +0200
Subject: [PATCH 17/24] fix(spec): erratum batch and spec-consistency fixes
(#10, #11, #12, #14) (#25)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- #14: RFC 9562 replaces obsoleted RFC 4122; JSON-LD siblings advertised
and served as application/ld+json; alias mechanism described without
the undefined aliases.json filename; undefined tombstone rationale
fields dropped; UUID namespace derivation notes clarify NAMESPACE_DNS
is an opaque salt.
- #10 (option B): succession is provenance, not equivalence — tombstoned
records carry superseded_by (dcterms:isReplacedBy); MappingAssertion
stays reserved for work-level equivalence with a Work-IRI subject. New
compiler invariant: superseded_by only on withdrawn/blocked records.
- #11: published Work records now carry direct exactMatch/closeMatch
arrays derived from accepted mappings, so skos:exactMatch/closeMatch
edges exist in the JSON-LD graph alongside the reified assertions.
- #12 (option 1): authored SPDX ids are emitted as canonical SPDX IRIs
(https://spdx.org/licenses/{id}); dcterms:license is now IRI-typed for
both license and license_url. Non-SPDX values are omitted with a
compile warning (registry data already cleaned in registry#9).
- data/ submodule -> registry main 2c548b3 (SPDX license cleanup).
Co-authored-by: Moritz Mähr <14755525+maehr@users.noreply.github.com>
Co-authored-by: Claude Fable 5
---
data | 2 +-
package-lock.json | 8 +++
package.json | 1 +
public/contexts/v1.jsonld | 9 ++-
scripts/compile.ts | 59 +++++++++++++++++--
src/content/docs/get-started/url-layout.md | 6 +-
.../docs/standard/identifier-syntax.md | 4 +-
src/content/docs/standard/json-ld.md | 7 ++-
src/content/docs/standard/specification.md | 14 ++---
src/content/docs/standard/versioning.md | 12 ++--
src/pages/id/mapping/[uuid].json.ts | 2 +-
src/pages/id/mapping/[uuid]/index.astro | 2 +-
src/pages/id/ref/[uuid].json.ts | 2 +-
src/pages/id/ref/[uuid]/index.astro | 2 +-
src/pages/id/system/[key].json.ts | 2 +-
src/pages/id/system/[key]/index.astro | 2 +-
src/pages/id/work/[key].json.ts | 2 +-
src/pages/id/work/[key]/index.astro | 2 +-
standard/schema/common.ts | 3 +
standard/schema/work.ts | 6 +-
20 files changed, 114 insertions(+), 33 deletions(-)
diff --git a/data b/data
index d31ed0b..2c548b3 160000
--- a/data
+++ b/data
@@ -1 +1 @@
-Subproject commit d31ed0bb0d561d4e54224b3af1c730e851648491
+Subproject commit 2c548b30a25f7c1ecf110d774bc934fd5aa7a32b
diff --git a/package-lock.json b/package-lock.json
index 1076de6..38e4f43 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -29,6 +29,7 @@
"pagefind": "^1.5.2",
"prettier": "^3.8.3",
"prettier-plugin-astro": "^0.14.1",
+ "spdx-license-ids": "^3.0.23",
"starlight-links-validator": "^0.24.0",
"starlight-llms-txt": "^0.10.0",
"starlight-openapi": "^0.25.3",
@@ -9589,6 +9590,13 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/spdx-license-ids": {
+ "version": "3.0.23",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz",
+ "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
"node_modules/split": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz",
diff --git a/package.json b/package.json
index c4a40ba..172774b 100644
--- a/package.json
+++ b/package.json
@@ -48,6 +48,7 @@
"pagefind": "^1.5.2",
"prettier": "^3.8.3",
"prettier-plugin-astro": "^0.14.1",
+ "spdx-license-ids": "^3.0.23",
"starlight-links-validator": "^0.24.0",
"starlight-llms-txt": "^0.10.0",
"starlight-openapi": "^0.25.3",
diff --git a/public/contexts/v1.jsonld b/public/contexts/v1.jsonld
index 04fb67a..491d357 100644
--- a/public/contexts/v1.jsonld
+++ b/public/contexts/v1.jsonld
@@ -67,11 +67,18 @@
"language": "dcterms:language",
"edition": "schema:bookEdition",
"access": "tr:access",
- "license": "dcterms:license",
+ "license": {
+ "@id": "dcterms:license",
+ "@type": "@id"
+ },
"license_url": {
"@id": "dcterms:license",
"@type": "@id"
},
+ "superseded_by": {
+ "@id": "dcterms:isReplacedBy",
+ "@type": "@id"
+ },
"last_checked": {
"@id": "tr:lastChecked",
"@type": "xsd:date"
diff --git a/scripts/compile.ts b/scripts/compile.ts
index 8a8c251..ab6ceff 100644
--- a/scripts/compile.ts
+++ b/scripts/compile.ts
@@ -9,6 +9,7 @@ import { join, basename, resolve } from 'node:path';
import { createHash } from 'node:crypto';
import { v5 as uuidv5 } from 'uuid';
import { parse as parseYaml } from 'yaml';
+import { createRequire } from 'node:module';
import {
Work,
CitationSystem,
@@ -19,6 +20,11 @@ import {
const REFERENCE_NS = 'b1a3670e-2ac7-544c-a1b9-396e0dc193f7';
const MAPPING_NS = 'f16bb214-4241-549d-ad41-7b011f02befb';
+const require = createRequire(import.meta.url);
+const spdxLicenseIds: string[] = require('spdx-license-ids');
+const spdxDeprecatedIds: string[] = require('spdx-license-ids/deprecated');
+const SPDX_IDS = new Set([...spdxLicenseIds, ...spdxDeprecatedIds]);
+
const projectRoot = resolve(process.cwd());
const dataRoot = join(projectRoot, 'data');
const distRoot = join(projectRoot, 'dist');
@@ -145,6 +151,7 @@ type WorkSource = {
status: string;
created: string;
modified: string;
+ superseded_by?: string;
creators?: CreatorSource[];
};
citation_system: string;
@@ -162,6 +169,7 @@ type SystemSource = {
status: string;
created: string;
modified: string;
+ superseded_by?: string;
// Per-chapter verse counts for chapter/verse systems. When present, the
// compiler exposes a `verseGlobal` template variable (cumulative 1..N
// across chapters) for resolvers whose anchors use a single running counter.
@@ -295,7 +303,17 @@ function buildResolverEntry(
if (resolver.edition !== undefined) entry.edition = resolver.edition;
if (resolver.provider !== undefined) entry.provider = resolver.provider;
entry.access = resolver.access ?? 'unknown';
- if (resolver.license !== undefined) entry.license = resolver.license;
+ if (resolver.license !== undefined) {
+ if (SPDX_IDS.has(resolver.license)) {
+ // Emit the canonical SPDX IRI so dcterms:license has a single
+ // IRI-typed range in the JSON-LD output.
+ entry.license = `https://spdx.org/licenses/${resolver.license}`;
+ } else {
+ console.warn(
+ `⚠ license "${resolver.license}" is not an SPDX id; omitted from output (use license_url for non-SPDX terms)`,
+ );
+ }
+ }
if (resolver.license_url !== undefined)
entry.license_url = resolver.license_url;
if (resolver.last_checked !== undefined)
@@ -373,6 +391,7 @@ export function compileRegistry(): CompiledRegistry {
status: src.status,
created: src.created,
modified: src.modified,
+ ...(src.superseded_by ? { superseded_by: src.superseded_by } : {}),
};
const parsed = CitationSystem.safeParse(record);
if (!parsed.success) {
@@ -399,6 +418,18 @@ export function compileRegistry(): CompiledRegistry {
);
}
+ // Direct SKOS mapping edges (skos:exactMatch / skos:closeMatch via the
+ // context) derived from the work's accepted mapping assertions, in
+ // addition to the reified MappingAssertion records below.
+ const exactMatches: string[] = [];
+ const closeMatches: string[] = [];
+ for (const m of src.mappings ?? []) {
+ if (m.status === 'withdrawn' || m.status === 'blocked') continue;
+ (m.relation === 'exactMatch' ? exactMatches : closeMatches).push(
+ m.identifier,
+ );
+ }
+
const workRecord = {
id: workIri,
key: workKey,
@@ -407,7 +438,12 @@ export function compileRegistry(): CompiledRegistry {
status: src.work.status,
created: src.work.created,
modified: src.work.modified,
+ ...(src.work.superseded_by
+ ? { superseded_by: src.work.superseded_by }
+ : {}),
...(src.work.creators ? { creators: src.work.creators } : {}),
+ ...(exactMatches.length ? { exactMatch: exactMatches } : {}),
+ ...(closeMatches.length ? { closeMatch: closeMatches } : {}),
};
const workParsed = Work.safeParse(workRecord);
if (!workParsed.success) {
@@ -533,6 +569,7 @@ export function compileRegistry(): CompiledRegistry {
type TombstoneRecord = {
id: string;
status: string;
+ superseded_by?: string;
};
const TOMBSTONE_STATUSES = new Set(['withdrawn']);
@@ -557,10 +594,24 @@ function enforceTombstoneInvariants(reg: {
const errors: string[] = [];
+ // superseded_by is a tombstone-only field: it MUST NOT appear on records
+ // that are still part of the live registry surface.
+ for (const r of all) {
+ if (
+ r.superseded_by !== undefined &&
+ r.status !== 'withdrawn' &&
+ r.status !== 'blocked'
+ ) {
+ errors.push(
+ `${r.id}: superseded_by is only allowed on withdrawn/blocked records (status: ${r.status})`,
+ );
+ }
+ }
+
// Active CanonicalReferences MUST NOT point at tombstoned work/system —
- // those break resolution. MappingAssertions are intentionally exempt:
- // successor links are carried by active exactMatch mappings whose subject
- // is the withdrawn IRI and whose target is the active successor.
+ // those break resolution. Successor links are carried by the tombstoned
+ // record's own superseded_by field (dcterms:isReplacedBy), not by
+ // MappingAssertions, which are reserved for work-level equivalence.
const isActive = (r: TombstoneRecord) => !TOMBSTONE_STATUSES.has(r.status);
for (const ref of reg.references) {
if (!isActive(ref)) continue;
diff --git a/src/content/docs/get-started/url-layout.md b/src/content/docs/get-started/url-layout.md
index cab425b..afe5eb8 100644
--- a/src/content/docs/get-started/url-layout.md
+++ b/src/content/docs/get-started/url-layout.md
@@ -37,7 +37,11 @@ Plato's _Republic_ 514a — the Stephanus passage where Socrates begins the Alle
There is no `Accept`-header content negotiation. Every HTML record page advertises its JSON-LD sibling in the document head:
```html
-
+
```
A client either reads that `` tag, or simply appends `.json` to the canonical URL. The JSON payload carries the JSON-LD `@context` at [`/contexts/v1.jsonld`](/contexts/v1.jsonld) and is valid JSON-LD by content.
diff --git a/src/content/docs/standard/identifier-syntax.md b/src/content/docs/standard/identifier-syntax.md
index 1310779..80ca217 100644
--- a/src/content/docs/standard/identifier-syntax.md
+++ b/src/content/docs/standard/identifier-syntax.md
@@ -20,7 +20,7 @@ The TextRefs reference namespace UUID is:
b1a3670e-2ac7-544c-a1b9-396e0dc193f7
```
-This namespace is derived from `uuidv5(uuid.NAMESPACE_DNS, "textrefs.org/reference")` and is frozen for `v0.1.0-draft`.
+This namespace is derived from `uuidv5(uuid.NAMESPACE_DNS, "textrefs.org/reference")` and is frozen for `v0.1.0-draft`. (`NAMESPACE_DNS` here is only a frozen UUID constant used as salt; UUIDv5 treats its name input as opaque bytes, so the slash in the name string is intentional and valid.)
## Seed sequence
@@ -109,7 +109,7 @@ The mapping namespace UUID is:
f16bb214-4241-549d-ad41-7b011f02befb
```
-This namespace is derived from `uuidv5(uuid.NAMESPACE_DNS, "textrefs.org/mapping")` and is frozen for `v0.1.0-draft`.
+This namespace is derived from `uuidv5(uuid.NAMESPACE_DNS, "textrefs.org/mapping")` and is frozen for `v0.1.0-draft`. (As above, `NAMESPACE_DNS` is a frozen salt constant; the name input is opaque bytes, not a DNS label.)
The seed string is the following three-field sequence, joined with single line feed characters and no trailing newline:
diff --git a/src/content/docs/standard/json-ld.md b/src/content/docs/standard/json-ld.md
index 077d195..de1a827 100644
--- a/src/content/docs/standard/json-ld.md
+++ b/src/content/docs/standard/json-ld.md
@@ -31,6 +31,8 @@ The MVP mapping relations map directly onto SKOS:
- `exactMatch` → `skos:exactMatch`
- `closeMatch` → `skos:closeMatch`
+Published `Work` records additionally carry direct `exactMatch` / `closeMatch` arrays derived from their accepted mapping assertions, so SKOS-aware consumers get real `skos:exactMatch` / `skos:closeMatch` edges from the work IRI to the mapped identifiers without dereferencing the reified `MappingAssertion` records.
+
Use `exactMatch` only when the mapped object identifies the same reference with sufficient precision. If there is uncertainty about segmentation, edition, translation, coverage, or locator alignment, use `closeMatch`. See [Specification §10](/standard/specification/#10-mappingassertion).
## The context document
@@ -78,12 +80,13 @@ Use `exactMatch` only when the mapped object identifies the same reference with
"language": "dcterms:language",
"edition": "schema:bookEdition",
"access": "tr:access",
- "license": "dcterms:license",
+ "license": { "@id": "dcterms:license", "@type": "@id" },
"license_url": { "@id": "dcterms:license", "@type": "@id" },
+ "superseded_by": { "@id": "dcterms:isReplacedBy", "@type": "@id" },
"last_checked": { "@id": "tr:lastChecked", "@type": "xsd:date" },
"locator_regex": "tr:locatorRegex"
}
}
```
-`key`, `work_key`, and `citation_system_key` are plain strings in the core JSON format. Rich bibliographic and authority data — catalogue records, edition histories, subject classifications — belongs in external systems and is connected to TextRefs records through `MappingAssertion`s. The one in-record exception is the optional `Work.creators` array, which carries minimal authorship for citation rendering (see [Specification §6](/standard/specification/#6-work)). The `license` term carries an SPDX identifier string; `license_url` (optional fallback) carries an IRI. `MappingAssertion.source` is a plain string in v0.1 — a structured **W3C PROV-O** mapping (`prov:wasDerivedFrom`) is reserved for a later context version.
+`key`, `work_key`, and `citation_system_key` are plain strings in the core JSON format. Rich bibliographic and authority data — catalogue records, edition histories, subject classifications — belongs in external systems and is connected to TextRefs records through `MappingAssertion`s. The one in-record exception is the optional `Work.creators` array, which carries minimal authorship for citation rendering (see [Specification §6](/standard/specification/#6-work)). The `license` term carries the canonical SPDX licence IRI (`https://spdx.org/licenses/{id}`, derived from the authored SPDX identifier at compile time); `license_url` (optional fallback for non-SPDX terms) carries an IRI. Both map to an IRI-typed `dcterms:license`. `MappingAssertion.source` is a plain string in v0.1 — a structured **W3C PROV-O** mapping (`prov:wasDerivedFrom`) is reserved for a later context version.
diff --git a/src/content/docs/standard/specification.md b/src/content/docs/standard/specification.md
index f548ad1..5861542 100644
--- a/src/content/docs/standard/specification.md
+++ b/src/content/docs/standard/specification.md
@@ -182,7 +182,7 @@ A `CanonicalReference` represents one atomized, **language-independent** referen
"edition": "SBL Greek New Testament",
"provider": "STEP Bible",
"access": "open",
- "license": "CC-BY-4.0"
+ "license": "https://spdx.org/licenses/CC-BY-4.0"
}
],
"status": "candidate",
@@ -212,7 +212,7 @@ Required: `id`, `type` (`CanonicalReference`), `work_key`, `citation_system_key`
"edition": "King James Version",
"provider": "Bible Gateway",
"access": "open",
- "license": "CC0-1.0",
+ "license": "https://spdx.org/licenses/CC0-1.0",
"license_url": null,
"last_checked": "2026-01-01"
}
@@ -223,7 +223,7 @@ Required per entry: `url`, `access`.
- `url` MUST be a dereferenceable external IRI ([RFC 3987](https://www.rfc-editor.org/rfc/rfc3987)).
- `language` MUST be present when the entry is language-specific (e.g. a translation), as a [BCP 47](https://www.rfc-editor.org/info/bcp47) language tag ([RFC 5646](https://www.rfc-editor.org/rfc/rfc5646)). Tags MUST include an [ISO 15924](https://www.unicode.org/iso15924/) script subtag when the entry uses a non-default script for the language (e.g. `grc-Grek`, `hbo-Hebr`, `grc-Latn`). `edition` SHOULD name the specific edition or version when known.
- `access` MUST be one of `open`, `paywalled`, `restricted`, `unknown`.
-- `license` SHOULD be a current [SPDX license identifier](https://spdx.org/licenses/) (e.g. `CC0-1.0`, `CC-BY-4.0`) when the licence of the target resource is known. For licences not in the SPDX list, omit `license` and use the optional `license_url` to point at the licence text.
+- `license` SHOULD be authored as a current [SPDX license identifier](https://spdx.org/licenses/) (e.g. `CC0-1.0`, `CC-BY-4.0`) when the licence of the target resource is known; published JSON-LD carries the canonical SPDX IRI (`https://spdx.org/licenses/{id}`), so `dcterms:license` has a single IRI-typed range. For licences not in the SPDX list, omit `license` and use the optional `license_url` to point at the licence text.
- Values implying permission to host copyrighted full text (e.g. a `license` of `proprietary` accompanied by hosted text) are forbidden; the no-text rule in [§2](#2-conformance) governs.
- A `CanonicalReference` whose `resolver_targets` is an empty array remains a valid identity record; adding or removing an entry MUST NOT change the parent reference's `id`.
- Tombstoning a single bad URL is done by removing the entry; tombstoning the whole reference uses the parent `status` field. There is no independent status on individual entries.
@@ -271,7 +271,7 @@ The persistence promise attaches at **promotion**: the first time a record is pu
An implementation MUST NOT silently change the identity-defining fields of an existing **promoted** `CanonicalReference`. Because those fields seed the deterministic identifier, any change produces a new `CanonicalReference` with a new identifier. The prior reference MUST be retained as a tombstone (`status` `deprecated` or `withdrawn`, [§12](#12-administrative-metadata)) and SHOULD be linked to its replacement through an `exactMatch` `MappingAssertion` ([§10](#10-mappingassertion)).
-A conforming registry SHOULD publish each `/id/{type}/{key}` IRI at two static URLs: the canonical URL itself (HTML for browsers) and a sibling with a `.json` extension carrying the JSON-LD payload. The HTML representation SHOULD advertise the JSON-LD sibling via `` in the document head. `Accept`-header content negotiation is not required.
+A conforming registry SHOULD publish each `/id/{type}/{key}` IRI at two static URLs: the canonical URL itself (HTML for browsers) and a sibling with a `.json` extension carrying the JSON-LD payload. The HTML representation SHOULD advertise the JSON-LD sibling via `` in the document head. `Accept`-header content negotiation is not required.
## 12. Administrative metadata
@@ -291,7 +291,7 @@ Every registry object MUST include:
- `candidate` — proposed but not yet accepted as stable. Promotion from `draft` attaches the persistence promise.
- `active` — accepted and recommended for use.
- `deprecated` — retained but no longer recommended.
- - `withdrawn` — removed from active use because it was erroneous or has been superseded. If a successor exists, it is linked by an `exactMatch` `MappingAssertion`; see [Versioning](/standard/versioning/) for tombstones.
+ - `withdrawn` — removed from active use because it was erroneous or has been superseded. If a successor exists, the record's `superseded_by` field carries the successor IRI; see [Versioning](/standard/versioning/) for tombstones.
- `blocked` — retained as a visible tombstone because of a rights, trust, or policy dispute.
Deprecated, withdrawn, and blocked records SHOULD remain visible unless removal is required for legal, privacy, or safety reasons.
@@ -339,7 +339,7 @@ This is the case that motivates separating identity from location. The New Testa
"edition": "SBL Greek New Testament",
"provider": "STEP Bible",
"access": "open",
- "license": "CC-BY-4.0"
+ "license": "https://spdx.org/licenses/CC-BY-4.0"
},
{
"url": "https://www.biblegateway.com/passage/?search=John%203%3A16&version=KJV",
@@ -399,7 +399,7 @@ This standard relies on the following external standards. Each is normative wher
| Dates | [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) |
| URIs | [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986) |
| IRIs | [RFC 3987](https://www.rfc-editor.org/rfc/rfc3987) |
-| UUIDs | [RFC 4122](https://www.rfc-editor.org/rfc/rfc4122) |
+| UUIDs | [RFC 9562](https://www.rfc-editor.org/rfc/rfc9562) |
| Unicode normalization (NFC) | [Unicode Standard Annex #15](https://www.unicode.org/reports/tr15/) |
| Regular expression dialect | [ECMA-262](https://262.ecma-international.org/) §22.2 |
| Versioning | [SemVer 2.0.0](https://semver.org/spec/v2.0.0.html) |
diff --git a/src/content/docs/standard/versioning.md b/src/content/docs/standard/versioning.md
index 0c59a5e..c975c23 100644
--- a/src/content/docs/standard/versioning.md
+++ b/src/content/docs/standard/versioning.md
@@ -86,15 +86,15 @@ Registry identity is permanent once promoted: the IRI of a `Work`, `CitationSyst
### Schema
-Tombstones use one status value, no extra fields. The old record stays in the data tree with `status: withdrawn`. If a successor exists, a single `MappingAssertion` with `relation: exactMatch`, `subject: `, and `target: ` carries the link. Consumers walk the mapping to find the successor.
+Tombstones use one status value plus one optional field. The old record stays in the data tree with `status: withdrawn`. If a successor exists, the tombstoned record carries its IRI in `superseded_by` (`dcterms:isReplacedBy` in the published context). Consumers follow `superseded_by` to find the successor. `MappingAssertion`s are reserved for genuine work-level equivalence claims and MUST NOT be used for succession links.
### On-disk representation
-Tombstones are full records, not deletions. The old record retains every other field unchanged; only `status` flips to `withdrawn` and `modified` is bumped. If a successor exists, the successor is a separately authored record at the new IRI, and the linking `MappingAssertion` is committed alongside.
+Tombstones are full records, not deletions. The old record retains every other field unchanged; `status` flips to `withdrawn`, `modified` is bumped, and `superseded_by` is set when a successor exists. The successor is a separately authored record at the new IRI.
### HTTP behavior
-Old IRI HTML pages render a tombstone banner. The page already lists every `MappingAssertion` whose subject is this record, so the successor (if any) appears in that list with no extra rendering logic. The `.json` JSON-LD sibling returns the withdrawn record verbatim. Old IRIs are **not** hard-redirected: archival consumers MUST be able to inspect the tombstone payload.
+Old IRI HTML pages render a tombstone banner; when `superseded_by` is present the banner links the successor IRI. The `.json` JSON-LD sibling returns the withdrawn record verbatim. Old IRIs are **not** hard-redirected: archival consumers MUST be able to inspect the tombstone payload.
### Export inclusion
@@ -102,14 +102,14 @@ Tombstones MUST appear in monthly exports inside the same `.jsonl` file as their
### Compiler invariants
-The compiler enforces: an active `CanonicalReference` MUST NOT reference a tombstoned `Work` or `CitationSystem` through `work_key` or `citation_system_key`. `MappingAssertion`s are exempt — successor links from a withdrawn subject to an active target are exactly the documented pattern.
+The compiler enforces: an active `CanonicalReference` MUST NOT reference a tombstoned `Work` or `CitationSystem` through `work_key` or `citation_system_key`. `superseded_by` MUST only appear on records whose `status` is `withdrawn` or `blocked`.
Analogously, a promoted (`candidate` or higher) record MUST NOT reference a `draft` `Work` or `CitationSystem` through its keys: systems and works are promoted before or together with the records that depend on them.
### Aliases vs. tombstones
-`aliases.json` handles **presentational** URL aliases (multiple paths pointing at the same canonical record). Tombstones handle **identity** changes (the record itself is no longer canonical). These are distinct mechanisms and MUST NOT be conflated.
+The compiler maintains a presentational alias map (multiple lookup paths — external identifiers, `{work_key}/{locator}` pairs — pointing at the same canonical record). Tombstones handle **identity** changes (the record itself is no longer canonical). These are distinct mechanisms and MUST NOT be conflated.
## Rights and content guardrails
-Exports MUST NOT contain primary full text, commentary, apparatus, or rights metadata that implies TextRefs may redistribute copyrighted text. Disputed resolver endpoints remain in exports with `status: blocked` and tombstone rationale fields.
+Exports MUST NOT contain primary full text, commentary, apparatus, or rights metadata that implies TextRefs may redistribute copyrighted text. Disputed resolver endpoints remain in exports with `status: blocked`.
diff --git a/src/pages/id/mapping/[uuid].json.ts b/src/pages/id/mapping/[uuid].json.ts
index fa556eb..a592a66 100644
--- a/src/pages/id/mapping/[uuid].json.ts
+++ b/src/pages/id/mapping/[uuid].json.ts
@@ -16,6 +16,6 @@ export const GET: APIRoute = ({ props }) => {
};
const body = { '@context': CONTEXT, ...mapping };
return new Response(JSON.stringify(body, null, 2), {
- headers: { 'Content-Type': 'application/json; charset=utf-8' },
+ headers: { 'Content-Type': 'application/ld+json; charset=utf-8' },
});
};
diff --git a/src/pages/id/mapping/[uuid]/index.astro b/src/pages/id/mapping/[uuid]/index.astro
index e000127..cc963f8 100644
--- a/src/pages/id/mapping/[uuid]/index.astro
+++ b/src/pages/id/mapping/[uuid]/index.astro
@@ -35,7 +35,7 @@ const jsonHref = `/id/mapping/${uuidOf(mapping.id)}.json`;
tag: 'link',
attrs: {
rel: 'alternate',
- type: 'application/json',
+ type: 'application/ld+json',
href: jsonHref,
},
},
diff --git a/src/pages/id/ref/[uuid].json.ts b/src/pages/id/ref/[uuid].json.ts
index eea9e56..05823f0 100644
--- a/src/pages/id/ref/[uuid].json.ts
+++ b/src/pages/id/ref/[uuid].json.ts
@@ -14,6 +14,6 @@ export const GET: APIRoute = ({ props }) => {
const { ref } = props as { ref: ReturnType[number] };
const body = { '@context': CONTEXT, ...ref };
return new Response(JSON.stringify(body, null, 2), {
- headers: { 'Content-Type': 'application/json; charset=utf-8' },
+ headers: { 'Content-Type': 'application/ld+json; charset=utf-8' },
});
};
diff --git a/src/pages/id/ref/[uuid]/index.astro b/src/pages/id/ref/[uuid]/index.astro
index c82771f..910d1a8 100644
--- a/src/pages/id/ref/[uuid]/index.astro
+++ b/src/pages/id/ref/[uuid]/index.astro
@@ -63,7 +63,7 @@ const jsonHref = `/id/ref/${uuidOf(ref.id)}.json`;
tag: 'link',
attrs: {
rel: 'alternate',
- type: 'application/json',
+ type: 'application/ld+json',
href: jsonHref,
},
},
diff --git a/src/pages/id/system/[key].json.ts b/src/pages/id/system/[key].json.ts
index d5b5ff8..478e817 100644
--- a/src/pages/id/system/[key].json.ts
+++ b/src/pages/id/system/[key].json.ts
@@ -16,6 +16,6 @@ export const GET: APIRoute = ({ props }) => {
};
const body = { '@context': CONTEXT, ...system };
return new Response(JSON.stringify(body, null, 2), {
- headers: { 'Content-Type': 'application/json; charset=utf-8' },
+ headers: { 'Content-Type': 'application/ld+json; charset=utf-8' },
});
};
diff --git a/src/pages/id/system/[key]/index.astro b/src/pages/id/system/[key]/index.astro
index 26bc943..dc86e88 100644
--- a/src/pages/id/system/[key]/index.astro
+++ b/src/pages/id/system/[key]/index.astro
@@ -55,7 +55,7 @@ const jsonHref = `/id/system/${system.key}.json`;
tag: 'link',
attrs: {
rel: 'alternate',
- type: 'application/json',
+ type: 'application/ld+json',
href: jsonHref,
},
},
diff --git a/src/pages/id/work/[key].json.ts b/src/pages/id/work/[key].json.ts
index d31a8cb..18546a0 100644
--- a/src/pages/id/work/[key].json.ts
+++ b/src/pages/id/work/[key].json.ts
@@ -14,6 +14,6 @@ export const GET: APIRoute = ({ props }) => {
const { work } = props as { work: ReturnType[number] };
const body = { '@context': CONTEXT, ...work };
return new Response(JSON.stringify(body, null, 2), {
- headers: { 'Content-Type': 'application/json; charset=utf-8' },
+ headers: { 'Content-Type': 'application/ld+json; charset=utf-8' },
});
};
diff --git a/src/pages/id/work/[key]/index.astro b/src/pages/id/work/[key]/index.astro
index 09da5bc..a2b8fb0 100644
--- a/src/pages/id/work/[key]/index.astro
+++ b/src/pages/id/work/[key]/index.astro
@@ -95,7 +95,7 @@ const jsonHref = `/id/work/${work.key}.json`;
tag: 'link',
attrs: {
rel: 'alternate',
- type: 'application/json',
+ type: 'application/ld+json',
href: jsonHref,
},
},
diff --git a/standard/schema/common.ts b/standard/schema/common.ts
index 518aec4..ca6cacd 100644
--- a/standard/schema/common.ts
+++ b/standard/schema/common.ts
@@ -19,6 +19,9 @@ export const AdminMetadata = z.object({
status: Status,
created: IsoDate,
modified: IsoDate,
+ // Successor link on tombstoned records (dcterms:isReplacedBy). MUST only
+ // appear when status is withdrawn or blocked.
+ superseded_by: Iri.optional(),
});
export const FlatKey = z
diff --git a/standard/schema/work.ts b/standard/schema/work.ts
index aa643bf..0773ebe 100644
--- a/standard/schema/work.ts
+++ b/standard/schema/work.ts
@@ -1,5 +1,5 @@
import { z } from 'zod';
-import { AdminMetadata, FlatKey } from './common.js';
+import { AdminMetadata, FlatKey, Iri } from './common.js';
export const Creator = z.discriminatedUnion('kind', [
z.object({
@@ -21,6 +21,10 @@ export const WorkBase = AdminMetadata.extend({
type: z.literal('Work'),
preferred_label: z.string().min(1),
creators: z.array(Creator).optional(),
+ // Direct SKOS mapping edges derived from accepted MappingAssertions
+ // (skos:exactMatch / skos:closeMatch in the published context).
+ exactMatch: z.array(Iri).optional(),
+ closeMatch: z.array(Iri).optional(),
});
export const Work = WorkBase.superRefine((w, ctx) => {
From 29c15e46af51844fe27552cf69b8f415087a3819 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
Date: Sun, 5 Jul 2026 22:30:12 +0200
Subject: [PATCH 18/24] feat(site): flag draft records and exclude them from
search indexing (#26)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
ADR-0003 render follow-up: /id/ record pages show a draft notice and
emit robots noindex while status is draft; /reg/ listing pages inherit
the flag from their work/system. The tombstone banner now follows the
superseded_by successor link (was: exactMatch mapping prose) and covers
blocked records.
Co-authored-by: Moritz Mähr <14755525+maehr@users.noreply.github.com>
Co-authored-by: Claude Fable 5
---
src/components/Tombstone.astro | 52 +++++++++++++++++---
src/pages/id/mapping/[uuid]/index.astro | 11 ++++-
src/pages/id/ref/[uuid]/index.astro | 11 ++++-
src/pages/id/system/[key]/index.astro | 11 ++++-
src/pages/id/work/[key]/index.astro | 11 ++++-
src/pages/reg/system/[key]/refs/[page].astro | 11 +++++
src/pages/reg/work/[key]/refs/[page].astro | 11 +++++
7 files changed, 107 insertions(+), 11 deletions(-)
diff --git a/src/components/Tombstone.astro b/src/components/Tombstone.astro
index 34174ee..d161094 100644
--- a/src/components/Tombstone.astro
+++ b/src/components/Tombstone.astro
@@ -1,23 +1,48 @@
---
interface Props {
status: string;
+ supersededBy?: string;
}
-const { status } = Astro.props;
+const { status, supersededBy } = Astro.props;
-const isTombstone = status === 'withdrawn';
+const isDraft = status === 'draft';
+const isTombstone = status === 'withdrawn' || status === 'blocked';
---
+{
+ isDraft && (
+
+ )
+}
+
{
isTombstone && (
)
}
@@ -36,4 +61,17 @@ const isTombstone = status === 'withdrawn';
.reg-tombstone p {
margin: 0.25rem 0;
}
+ .reg-draft {
+ border-left: 4px solid var(--sl-color-blue, #2563eb);
+ background: var(--sl-color-bg-nav, rgba(37, 99, 235, 0.08));
+ padding: 0.75rem 1rem;
+ margin: 1rem 0;
+ border-radius: 0.25rem;
+ }
+ .reg-draft-title {
+ margin-top: 0;
+ }
+ .reg-draft p {
+ margin: 0.25rem 0;
+ }
diff --git a/src/pages/id/mapping/[uuid]/index.astro b/src/pages/id/mapping/[uuid]/index.astro
index cc963f8..8b1c276 100644
--- a/src/pages/id/mapping/[uuid]/index.astro
+++ b/src/pages/id/mapping/[uuid]/index.astro
@@ -39,6 +39,15 @@ const jsonHref = `/id/mapping/${uuidOf(mapping.id)}.json`;
href: jsonHref,
},
},
+ // Draft records are excluded from search indexing (ADR-0003).
+ ...(mapping.status === 'draft'
+ ? [
+ {
+ tag: 'meta' as const,
+ attrs: { name: 'robots', content: 'noindex' },
+ },
+ ]
+ : []),
],
}}
>
@@ -49,7 +58,7 @@ const jsonHref = `/id/mapping/${uuidOf(mapping.id)}.json`;
JSON
{
@@ -155,4 +192,4 @@ const jsonHref = `/id/ref/${uuidOf(ref.id)}.json`;
)
}
-
+
From c4b623a93ee8be823ac508a6bfe3a7f0fc1f02b9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
<14755525+maehr@users.noreply.github.com>
Date: Mon, 6 Jul 2026 08:15:47 +0200
Subject: [PATCH 23/24] chore(release): regenerate changelog with Astro 7 and
slim ref pages
Co-Authored-By: Claude Fable 5
---
CHANGELOG.md | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b269a56..8919633 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,7 @@
All notable changes to this project are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-## [0.1.0] - 2026-07-05
+## [0.1.0] - 2026-07-06
### Added
@@ -11,6 +11,8 @@ All notable changes to this project are documented in this file. The format is b
- Draft lifecycle with retractable pre-promotion identity (ADR-0003) (#23)
- Require explicit canonical ASCII digit and case forms in profiles (#24)
- Flag draft records and exclude them from search indexing (#26)
+- Upgrade to Astro 7 / Starlight 0.41, all dependencies to latest (#29)
+- Minimal static templates for reference pages (#30)
### Documentation
From 3dcff6f5932a945ee58f0f8d48b31db5937e6d1f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
<14755525+maehr@users.noreply.github.com>
Date: Mon, 6 Jul 2026 08:24:07 +0200
Subject: [PATCH 24/24] chore(data): bump submodule to registry main (c0a3275,
ORCID docs)
Co-Authored-By: Claude Fable 5
---
data | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/data b/data
index 2c548b3..c0a3275 160000
--- a/data
+++ b/data
@@ -1 +1 @@
-Subproject commit 2c548b30a25f7c1ecf110d774bc934fd5aa7a32b
+Subproject commit c0a3275ad21f76c68c7a375819f8833681f0e87e