))}
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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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
))}
diff --git a/src/pages/reg/index.astro b/src/pages/reg/index.astro
index e9aa101..9eca75d 100644
--- a/src/pages/reg/index.astro
+++ b/src/pages/reg/index.astro
@@ -19,13 +19,14 @@ const systems = [...loadSystems()].sort((a, b) => a.key.localeCompare(b.key));
>
The TextRefs registry holds works (texts that get cited),
+ {' '}
citation systems (rules for how locators inside a work look),
and canonical references (individual points inside a work). Each
record lives at its persistent identifier under /id/; this page
- is the human entry point for finding them. See
- URL layout for how
- /id/, /reg/, /cite/, and
- /api/ fit together, or the
+ is the human entry point for finding them. See{' '}
+ URL layout for how{' '}
+ /id/, /reg/, /cite/, and{' '}
+ /api/ fit together, or the{' '}
specification for the data model.
From 03cee88ea25f4d3ffc44ccca6f5f2f832e31be75 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
Date: Fri, 31 Jul 2026 14:50:18 +0200
Subject: [PATCH 28/45] fix(site): list works instead of references on
CitationSystem pages (#55) (#56)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
CitationSystem pages listed every canonical reference expressed in the
system — 27k for homer-book-line — which is both an information-model
mismatch (a system is associated with works; references are individual
locations) and the reason those pages ran long.
The section is now "Works using this system": each associated work once,
linked to its Work page, with the number of references it contributes and
an empty state when a system has no works. Individual references stay
reachable from Work pages (/reg/work//refs/) and /id/ref//.
The paginated /reg/system//refs// route is removed with it —
the preview section was its only inbound link, so it would otherwise
build orphan pages.
Fixes #55
Co-authored-by: Moritz Mähr <14755525+maehr@users.noreply.github.com>
Co-authored-by: Claude Opus 5
---
src/pages/id/system/[key]/index.astro | 73 ++++++-------
src/pages/reg/system/[key]/refs/[page].astro | 106 -------------------
2 files changed, 35 insertions(+), 144 deletions(-)
delete mode 100644 src/pages/reg/system/[key]/refs/[page].astro
diff --git a/src/pages/id/system/[key]/index.astro b/src/pages/id/system/[key]/index.astro
index 6062a19..a14a559 100644
--- a/src/pages/id/system/[key]/index.astro
+++ b/src/pages/id/system/[key]/index.astro
@@ -4,13 +4,11 @@ import Tombstone from '../../../../components/Tombstone.astro';
import {
loadSystems,
loadReferences,
- uuidOf,
+ loadWorks,
} from '../../../../lib/registry.ts';
import { UNSTABLE_BANNER } from '../../../../lib/banner.ts';
export async function getStaticPaths() {
- const PAGE_SIZE = 200;
- const PREVIEW = 20;
const systems = loadSystems();
const refs = loadReferences();
const refsBySystem = new Map();
@@ -19,27 +17,36 @@ export async function getStaticPaths() {
if (arr) arr.push(r);
else refsBySystem.set(r.citation_system_key, [r]);
}
+ const worksByKey = new Map(loadWorks().map((w) => [w.key, w]));
+
return systems.map((system) => {
- const systemRefs = (refsBySystem.get(system.key) ?? [])
- .slice()
+ const refCountByWorkKey = new Map();
+ for (const r of refsBySystem.get(system.key) ?? []) {
+ refCountByWorkKey.set(
+ r.work_key,
+ (refCountByWorkKey.get(r.work_key) ?? 0) + 1,
+ );
+ }
+ const works = Array.from(refCountByWorkKey.entries())
+ .map(([key, refCount]) => ({ work: worksByKey.get(key), refCount }))
+ .filter(
+ (e): e is { work: NonNullable; refCount: number } =>
+ Boolean(e.work),
+ )
.sort((a, b) =>
- a.locator.localeCompare(b.locator, undefined, { numeric: true }),
+ a.work.preferred_label.localeCompare(b.work.preferred_label),
);
- const pageCount = Math.max(1, Math.ceil(systemRefs.length / PAGE_SIZE));
return {
params: { key: system.key },
props: {
system,
- preview: systemRefs.slice(0, PREVIEW),
- refCount: systemRefs.length,
- pageCount,
- pageSize: PAGE_SIZE,
+ works,
},
};
});
}
-const { system, preview, refCount, pageCount, pageSize } = Astro.props;
+const { system, works } = Astro.props;
const jsonHref = `/id/system/${system.key}.json`;
---
@@ -84,30 +91,20 @@ const jsonHref = `/id/system/${system.key}.json`;
-
-
-
From b365cfc549605d2584afa22a41caa4d5471fc75d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
Date: Tue, 4 Aug 2026 16:48:43 +0200
Subject: [PATCH 29/45] =?UTF-8?q?feat(standard)!:=20collapse=20record=20li?=
=?UTF-8?q?fecycle=20to=20draft=20=E2=86=92=20active=20(ADR-0004)=20(#62)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat(standard)!: collapse record lifecycle to draft → active (ADR-0004)
Remove the `candidate` record status. Promotion out of `draft` now grants
both recommendation and identifier permanence in one expert-review gate,
so there is no longer a state that is permanent but not recommended.
- ADR-0004 records the decision; ADR-0003 is marked superseded by it.
- `Status` enum drops `candidate` in `standard/schema/common.ts` and in
its duplicate at `api/openapi.yaml`.
- Compiler invariant comments move to the two-state vocabulary; the
logic already keyed off `status !== 'draft'` and is unchanged.
- Specification §11/§12 state the new ladder and add the corrections
path for promoted records: never delete, never mutate identity fields,
move to deprecated/withdrawn/blocked with `superseded_by`. §14 gains a
validator check that no active record depends on a draft one.
- Governance §4.2/§5.1/§5.2/§5.3/§6 amended in English and in the legally
binding German text, in sync as ADR-0003 required.
No data migration: every record in the registry is `draft`, so nothing
was on `candidate`. Breaking for dump consumers reading the enum.
Refs #22
Co-Authored-By: Claude Opus 5
* fix(standard): resolve ADR-0004 self-references before merge
The ADR quoted its own pre-change state as if it were current: a stale
line-number citation to standard/schema/common.ts (the enum moved when
the lifecycle comment was added above it), and a quoted scripts/compile.ts
comment that this same PR rewrites. Both go stale the moment the PR
merges, since the ADR and the code change land together. Flagged by
Copilot review on #62.
Status flips Proposed -> Accepted per decisions/README.md's merge step,
matching ADR-0001/0002 which are already Accepted while only in staging.
Refs #22
---------
Co-authored-by: Moritz Mähr <14755525+maehr@users.noreply.github.com>
Co-authored-by: Claude Opus 5
---
CONTRIBUTING.md | 4 +-
README.md | 2 +-
ROADMAP.md | 2 +-
api/openapi.yaml | 2 +-
...0003-draft-lifecycle-ephemeral-identity.md | 4 +-
decisions/ADR-0004-lean-lifecycle.md | 83 +++++++++++++++++++
scripts/compile.ts | 6 +-
src/components/Tombstone.astro | 8 +-
src/content/docs/association/governance.md | 16 ++--
src/content/docs/community/contributing.md | 10 ++-
src/content/docs/community/roadmap.md | 2 +-
src/content/docs/de/association/governance.md | 16 ++--
src/content/docs/get-started/authoring.md | 6 +-
src/content/docs/get-started/how-it-works.md | 6 +-
.../docs/standard/identifier-syntax.md | 2 +-
src/content/docs/standard/specification.md | 18 ++--
src/content/docs/standard/versioning.md | 6 +-
src/lib/registry.fixture.ts | 8 +-
src/pages/id/ref/[uuid]/index.astro | 9 +-
standard/schema/common.ts | 4 +-
20 files changed, 150 insertions(+), 64 deletions(-)
create mode 100644 decisions/ADR-0004-lean-lifecycle.md
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 37f7e58..041b045 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -29,7 +29,7 @@ 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, 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.
+- **Expert review** — new works, new citation systems, new corpora, contested mappings, changes to deterministic ID inputs, status changes (promotion `draft` → `active`, and `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
@@ -48,7 +48,7 @@ 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).
+New registry records enter at `status: draft` after technical review; they stay retractable until an expert review promotes them to `active`, which permanently freezes their identifier (see the [versioning rules](https://textrefs.org/standard/versioning/) and governance §5).
## Local development
diff --git a/README.md b/README.md
index 1c74995..b10d48e 100644
--- a/README.md
+++ b/README.md
@@ -117,7 +117,7 @@ Both deposits live in the [TextRefs Zenodo community](https://zenodo.org/communi
## Roadmap
-TextRefs is **pre-1.0**: the association is being founded, the standard is being drafted, and the current registry examples are candidate data. Public milestones will appear on the [GitHub project board](https://github.com/textrefs/textrefs.org/projects) once it is set up. The statutes ([English](https://textrefs.org/association/statutes/), [Deutsch](https://textrefs.org/de/association/statutes/)) and governance regulation describe the long-term scope.
+TextRefs is **pre-1.0**: the association is being founded, the standard is being drafted, and the current registry examples are draft data. Public milestones will appear on the [GitHub project board](https://github.com/textrefs/textrefs.org/projects) once it is set up. The statutes ([English](https://textrefs.org/association/statutes/), [Deutsch](https://textrefs.org/de/association/statutes/)) and governance regulation describe the long-term scope.
## Contributing
diff --git a/ROADMAP.md b/ROADMAP.md
index cd5c91c..08fb26d 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -4,7 +4,7 @@ This is the public roadmap. The same content is mirrored on the docs site at r.status !== 'draft';
+```
+
+The logic survives a two-state ladder unchanged. The real cost of keeping the state is prose: specification §11–§12, the versioning maturity-ladder prose, identifier-syntax, get-started examples, CONTRIBUTING, and — because it is the legally binding text — the German governance regulation, which ADR-0003 itself records as amended in lockstep with the English original (`association/governance.md` §4.2, §5.1, §5.2, §5.3, §6, and its `de/` counterpart).
+
+## Decision drivers
+
+- A status vocabulary should have no state whose meaning must be explained by what it is _not_: `candidate` is defined by "not yet `active`", not by anything it uniquely permits or forbids.
+- Promotion should be one reviewable event, not two: expert review under governance §4.2 already gates both "permanent" and "recommended" judgments together in practice.
+- The identifier-persistence promise must stay absolute where it applies; ADR-0003's finding — that a promise with fuzzy edges is worse than a narrower hard one — argues against a state whose only content is "already permanent, not yet endorsed".
+- ADR-0002's offline-computable identity (deterministic v5 UUIDs from `(work_key, citation_system_key, locator)`) must hold at every remaining stage; collapsing the ladder does not touch the identity seed.
+- Additional citation systems (issue [#60](https://github.com/textrefs/textrefs.org/issues/60), ADR-0005) need to be introducible as drafts against an already-active work, which requires a clear per-record status _dependency_ rule rather than a total ordering of ladder rungs.
+
+## Considered options
+
+1. **Keep the three-state ladder — status quo.** The extra state keeps costing prose and review ceremony (two governance sections, two spec sections, a compiler comment, a fixture value) without buying a distinction anyone consumes: no tooling, no rendering, and no policy in this repository treats `candidate` differently from `active` except for the recommendation flag.
+2. **Keep `candidate` but attach permanence at `candidate → active` instead of `draft → candidate`.** This makes `candidate` mutable, which contradicts ADR-0003's core finding that review-stage records must be cheap to correct (§ Ephemerality: "Draft records ... MAY be corrected ... or retracted ... without a tombstone"). Moving that ephemerality window one rung up just relabels which state absorbs the correction cost, and reintroduces the exact ambiguity ADR-0003 was written to remove — a published-but-uncommitted state with no name for its own promise.
+3. **Collapse to `draft` → `active`.** ← chosen
+4. **Rename `candidate` to something clearer** (e.g. `reviewed`, `accepted`). Cosmetic: the redundancy is structural — a promoted-but-unrecommended tier that nothing uses — not lexical. A better name does not give the state a purpose.
+
+## Decision
+
+We choose **Option 3**. The status ladder is `draft` → `active`, plus the tombstone states `deprecated`, `withdrawn`, `blocked`. The surviving mutable-review keyword is `draft`, not `candidate` — ADR-0003's ephemeral tier is unchanged, only the rung above it is removed.
+
+**`draft`.** Unchanged from ADR-0003: mutable, not recommended, excluded from the persistence policy (specification §11), retractable without a tombstone.
+
+**`active`.** Promotion out of `draft` now means _both_ "TextRefs recommends this record" _and_ "this identifier is now permanent". These were always judged together under governance §4.2 expert review; there is no longer a state that carries one without the other. One event, one gate: expert review, governance §4.2.
+
+**Corrections after promotion.** An incorrect `active` record is never deleted and its identity-defining fields are never mutated — that constraint is unchanged from specification §11. It is marked `deprecated` (retained, no longer recommended), `withdrawn` (erroneous or superseded), or `blocked` (rights, trust, or policy dispute), and carries `superseded_by` when a successor exists. Because identity is deterministic (ADR-0002), the corrected tuple mints a _new_ UUID at a new IRI; the old IRI keeps resolving as a tombstone page. This is the only correction path for a promoted record: no identity mutation, no deletion.
+
+**General dependency rule.** An `active` record MUST NOT depend on a `draft` record: an active `CanonicalReference` needs an active `Work` and an active `CitationSystem`; an active `MappingAssertion` needs an active subject `Work`. This restates `scripts/compile.ts:631`'s existing invariant in terms of the two-state ladder instead of "candidate or higher". ADR-0005 specialises this rule for the preferred-versus-additional citation-system distinction it introduces.
+
+**Governance.** `association/governance.md` (and its legally binding `de/association/governance.md` counterpart) lose the `candidate` row in §5.1 and §5.2; §4.2's promotion bullet becomes "promotion of records from `draft` to `active`"; §5.3's "Promoted IDs (status `candidate` or higher)" becomes "Active IDs"; §6 item 4 ("Provisional records are expressed through the `draft` status ... their IDs may disappear or change until promotion") follows without further change other than dropping any remaining `candidate` reference. Both texts are amended in the same change, as ADR-0003 required.
+
+## Consequences
+
+### Positive
+
+- One promotion event and one review gate, matching how governance §4.2 already treats the decision in practice.
+- The status vocabulary is now fully explained by two live states plus three tombstones — no state defined by what it is not.
+- No data migration: nothing in the `data/` submodule is on `candidate`.
+- ADR-0005's per-record dependency rules become expressible in one sentence each, instead of needing to reason about ladder position.
+- The spec carries `maturity: working-draft` (`/standard/versioning/`, "Unstable. Data model and prose may change without notice and without a version bump while the core is settled"), so this change needs no spec version bump.
+
+### Negative / trade-offs
+
+- Dump consumers reading the `Status` enum see one fewer value. Under `/standard/versioning/#semver-rules-for-data-packages` this is a breaking change for the data package's SemVer, exercised here under the pre-1.0 latitude ADR-0002 and ADR-0001 already established as acceptable.
+- Anyone who read ADR-0003 must now read two ADRs (ADR-0003 plus this one) to know the current lifecycle model; ADR-0003 itself is not rewritten, only superseded.
+- Two legally distinct texts (English `governance.md`, binding German `de/association/governance.md`) must be amended in sync again, repeating the coordination cost ADR-0003 already paid once.
+- A future registry need for a "proposed but already permanent" tier would have to re-add a state rather than repurpose an existing one.
+
+### Follow-up actions
+
+- [x] Remove `candidate` from the `Status` enum in `standard/schema/common.ts` and from `api/openapi.yaml:185`.
+- [x] Reword the compiler invariants and comments in `scripts/compile.ts` (e.g. the comment above line 631) to describe a `draft` → `active` ladder instead of "candidate or higher".
+- [x] Update `src/lib/registry.fixture.ts`, whose fixture records currently carry `status: 'candidate'`.
+- [x] Sweep the lifecycle prose: specification §11–§12, `/standard/versioning/`, `/standard/identifier-syntax/`, get-started examples, `CONTRIBUTING.md`, and its community-docs mirror.
+- [x] Amend governance §4.2, §5.1, §5.2, §5.3, and §6 in both `association/governance.md` (English) and `de/association/governance.md` (binding German).
+
+## Links
+
+- Related ADRs: ADR-0002 (deterministic identity from the semantic tuple), ADR-0003 (superseded by this one), ADR-0005 (preferred citation system — builds on the dependency rule this ADR states)
+- Related issues / PRs: textrefs/textrefs.org#22
+- External references: governance regulation §4–§6 (`/association/governance/`)
diff --git a/scripts/compile.ts b/scripts/compile.ts
index fef03be..f18315c 100644
--- a/scripts/compile.ts
+++ b/scripts/compile.ts
@@ -625,9 +625,9 @@ function enforceRegistryInvariants(reg: {
);
}
- // Analogously (ADR-0003): a promoted record — anything at `candidate` or
- // higher, so anything carrying the persistence promise — MUST NOT depend on
- // a record that is still retractable.
+ // Analogously (ADR-0004): a promoted record — anything past `draft`, so
+ // anything that carries or once carried the persistence promise — MUST NOT
+ // depend on a record that is still retractable.
const isPromoted = (r: StatusRecord) => r.status !== 'draft';
for (const ref of reg.references) {
if (!isPromoted(ref)) continue;
diff --git a/src/components/Tombstone.astro b/src/components/Tombstone.astro
index d161094..0a3cd14 100644
--- a/src/components/Tombstone.astro
+++ b/src/components/Tombstone.astro
@@ -17,10 +17,10 @@ const isTombstone = status === 'withdrawn' || status === 'blocked';
Draft record.
- This record has not been promoted by expert review. Its identifier
- carries no persistence promise yet: the record may be corrected (which
- mints a different identifier) or retracted without a tombstone. Do not
- cite it as stable.
+ This record has not been promoted to active by expert
+ review. Its identifier carries no persistence promise yet: the record
+ may be corrected (which mints a different identifier) or retracted
+ without a tombstone. Do not cite it as stable.
@@ -170,6 +179,11 @@ const jsonHref = `/id/work/${work.key}.json`;
{r.locator}{' '}
+ {multiSystem && (
+ <>
+ {r.citation_system_key}{' '}
+ >
+ )}
{r.resolver_targets.length} target(s)
diff --git a/src/pages/reg/work/[key]/refs/[page].astro b/src/pages/reg/work/[key]/refs/[page].astro
index 650b4b5..ebf3a98 100644
--- a/src/pages/reg/work/[key]/refs/[page].astro
+++ b/src/pages/reg/work/[key]/refs/[page].astro
@@ -20,9 +20,15 @@ export async function getStaticPaths() {
return works.flatMap((work) => {
const workRefs = (refsByWork.get(work.key) ?? [])
.slice()
- .sort((a, b) =>
- a.locator.localeCompare(b.locator, undefined, { numeric: true }),
+ .sort(
+ (a, b) =>
+ a.citation_system_key.localeCompare(b.citation_system_key) ||
+ a.locator.localeCompare(b.locator, undefined, { numeric: true }),
);
+ // A work cited under several systems can repeat a locator string while
+ // meaning a different passage each time (ADR-0005) — label them.
+ const multiSystem =
+ new Set(workRefs.map((r) => r.citation_system_key)).size > 1;
const pageCount = Math.max(1, Math.ceil(workRefs.length / PAGE_SIZE));
return Array.from({ length: pageCount }, (_, i) => {
const page = i + 1;
@@ -32,6 +38,7 @@ export async function getStaticPaths() {
props: {
work,
refs: slice,
+ multiSystem,
page,
pageCount,
pageSize: PAGE_SIZE,
@@ -42,7 +49,8 @@ export async function getStaticPaths() {
});
}
-const { work, refs, page, pageCount, pageSize, total } = Astro.props;
+const { work, refs, multiSystem, page, pageCount, pageSize, total } =
+ Astro.props;
const prev = page > 1 ? page - 1 : null;
const next = page < pageCount ? page + 1 : null;
const from = (page - 1) * pageSize + 1;
@@ -85,6 +93,11 @@ const to = Math.min(page * pageSize, total);
{r.locator}{' '}
+ {multiSystem && (
+ <>
+ {r.citation_system_key}{' '}
+ >
+ )}
{r.resolver_targets.length} target(s)
))
diff --git a/standard/schema/work.ts b/standard/schema/work.ts
index 044ff6c..e912992 100644
--- a/standard/schema/work.ts
+++ b/standard/schema/work.ts
@@ -20,6 +20,12 @@ export const WorkBase = AdminMetadata.extend({
key: FlatKey,
type: z.literal('Work'),
preferred_label: z.string().min(1),
+ // The citation system this work is cited under by default (ADR-0005). It
+ // governs the bare `/cite/{work}/{locator}` alias and default presentation
+ // only — it is identity-neutral and never affects how a fully qualified
+ // reference validates or resolves. The compiler checks that it names a
+ // known CitationSystem.
+ preferred_citation_system_key: FlatKey,
creators: z.array(Creator).optional(),
// Compiler-derived projection of the work's non-tombstoned
// MappingAssertions (skos:exactMatch / skos:closeMatch in the published
From 7a94c0eba422bd9df6ffb168c2ade30049cfbc70 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
Date: Wed, 12 Aug 2026 22:16:26 +0200
Subject: [PATCH 31/45] feat(standard)!: replace SKOS mapping relations with
alternateOf and isReferencedBy (ADR-0006) (#67)
---
.github/PULL_REQUEST_TEMPLATE.md | 2 +-
README.md | 2 +-
api/openapi.yaml | 14 ++-
.../ADR-0006-mapping-relation-vocabulary.md | 116 ++++++++++++++++++
public/contexts/v1.jsonld | 9 +-
scripts/compile.test.ts | 110 +++++++++++++++++
scripts/compile.ts | 26 ++--
scripts/source-schema.ts | 5 +-
src/content/docs/get-started/authoring.md | 4 +-
src/content/docs/get-started/how-it-works.md | 4 +-
.../mappings-and-resolver-targets.md | 10 +-
.../docs/standard/identifier-syntax.md | 2 +-
src/content/docs/standard/json-ld.md | 32 ++---
src/content/docs/standard/specification.md | 10 +-
src/lib/registry.fixture.ts | 21 +++-
standard/schema/mapping-assertion.ts | 3 +-
standard/schema/work.ts | 8 +-
17 files changed, 320 insertions(+), 58 deletions(-)
create mode 100644 decisions/ADR-0006-mapping-relation-vocabulary.md
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index fb0a100..61c1ea0 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -36,7 +36,7 @@
Re-minting changes a record's IRI. The old IRI must continue to resolve as a tombstone. See the [tombstones section in versioning.md](https://textrefs.org/standard/versioning/#tombstones-and-re-minted-records).
- [ ] Old record retained with `status: withdrawn`
-- [ ] A new `MappingAssertion` with `relation: exactMatch`, `subject: `, `target: ` links the old record to its successor (omit if there is no successor)
+- [ ] The old record's `superseded_by` field (`dcterms:isReplacedBy`) links it to its successor's IRI (omit if there is no successor)
- [ ] All other records that reference the old IRI have been audited (re-targeted to the new IRI, or themselves marked `withdrawn`)
- [ ] Commit message uses `feat!:` / `fix!:` or `feat(scope)!:` / `fix(scope)!:` to signal the breaking IRI change
diff --git a/README.md b/README.md
index b10d48e..823d7c5 100644
--- a/README.md
+++ b/README.md
@@ -132,7 +132,7 @@ Two release trains live in two repositories:
- **TextRefs Standard** (this repo) — tags `vMAJOR.MINOR.PATCH[-prerelease]` advance the spec, schemas, and site together. The spec's maturity level (`working-draft` → `candidate-recommendation` → `recommendation`) is encoded in each `/standard/*` page's frontmatter; the SemVer tag encodes pre-release status.
- **TextRefs Registry** ([`textrefs/registry`](https://github.com/textrefs/registry)) — calendar tags `vYYYY.MM.N` cut monthly registry exports. The data-package `version` inside `datapackage.json` follows SemVer-without-`v`.
-Records can be re-minted (e.g. when a `work` key is renamed). The old IRI continues to resolve as a tombstone (`status: withdrawn`); successors are linked by an `exactMatch` `MappingAssertion`. See [versioning policy](https://textrefs.org/standard/versioning/) for the full rules.
+Records can be re-minted (e.g. when a `work` key is renamed). The old IRI continues to resolve as a tombstone (`status: withdrawn`); successors are linked by the `superseded_by` field (`dcterms:isReplacedBy`). See [versioning policy](https://textrefs.org/standard/versioning/) for the full rules.
## Contributors and roles
diff --git a/api/openapi.yaml b/api/openapi.yaml
index 8f3df04..76bd354 100644
--- a/api/openapi.yaml
+++ b/api/openapi.yaml
@@ -217,17 +217,17 @@ components:
Governs the bare /cite/{work_key}/{locator} alias and default
presentation only; it is identity-neutral and never affects how
a fully qualified reference validates or resolves.
- exactMatch:
+ alternateOf:
type: array
items: { $ref: '#/components/schemas/Iri' }
description:
- Compiler-derived from the work's non-tombstoned exactMatch
+ Compiler-derived from the work's non-tombstoned alternateOf
MappingAssertions. Read-only, never authored.
- closeMatch:
+ isReferencedBy:
type: array
items: { $ref: '#/components/schemas/Iri' }
description:
- Compiler-derived from the work's non-tombstoned closeMatch
+ Compiler-derived from the work's non-tombstoned isReferencedBy
MappingAssertions. Read-only, never authored.
CitationSystem:
allOf:
@@ -294,7 +294,11 @@ components:
description: MUST be a Work IRI.
relation:
type: string
- enum: [exactMatch, closeMatch]
+ enum: [alternateOf, isReferencedBy]
+ description: Chosen by what the target is (ADR-0006). alternateOf
+ (prov:alternateOf) — another entity denoting the same work.
+ isReferencedBy (dcterms:isReferencedBy) — a document or page
+ about the work.
target:
type: object
required: [identifier]
diff --git a/decisions/ADR-0006-mapping-relation-vocabulary.md b/decisions/ADR-0006-mapping-relation-vocabulary.md
new file mode 100644
index 0000000..4d938a2
--- /dev/null
+++ b/decisions/ADR-0006-mapping-relation-vocabulary.md
@@ -0,0 +1,116 @@
+# ADR-0006: MappingAssertion relation vocabulary — `alternateOf` and `isReferencedBy`
+
+- **Status:** Proposed
+- **Date:** 2026-08-11
+- **Deciders:** @maehr
+- **Tags:** spec, data-model
+
+## Context and problem statement
+
+`MappingAssertion.relation` is a two-value enum — `z.enum(['exactMatch', 'closeMatch'])` (`standard/schema/mapping-assertion.ts:21`) — published as `skos:exactMatch` and `skos:closeMatch` (`public/contexts/v1.jsonld:40-47`). Two community issues filed by @stephenhart8, both marked breaking, argue that both values are wrong for the way the registry actually uses them. (Both issues name `v0.2.0-draft`; the repository is on `v0.1.0-draft`, `ROADMAP.md:7` and `specification.md:9`. See _Versioning_ below.)
+
+**[#59](https://github.com/textrefs/textrefs.org/issues/59)** — `skos:closeMatch` is the wrong property for linking a Work to a _page about_ it. Every one of the 12 `closeMatch` mappings in `data/works/*.yaml` is a Wikipedia article URL: a document that describes the work, not a weaker identifier for it. @stephenhart8 proposed `dcterms:isReferencedBy`; @julsraemy agreed.
+
+**[#58](https://github.com/textrefs/textrefs.org/issues/58)** — `skos:exactMatch` is a category error for Work ↔ Wikidata co-reference. SKOS mapping properties have domain and range `skos:Concept`; a `Work` is not a `skos:Concept`. @stephenhart8 proposed `owl:sameAs`. @julsraemy objected that `owl:sameAs` entails indiscernibility, symmetry and transitivity into the whole `sameAs` closure — pulling Wikidata's fused VIAF/GND identities and all their assertions in under TextRefs' semantics — and that `schema:sameAs` fails in the opposite direction, being underspecified and itself defined around a _reference web page_, which is closer to #59's relation. The thread converged on three acceptable candidates (`skos:exactMatch` retained, a minted `equivalent` à la Linked Art, or `prov:alternateOf`), and both @julsraemy and @stephenhart8 ruled out `skos:exactMatch` as genuinely wrong rather than cosmetically imprecise.
+
+The two issues are one change. `relation` appears in the authoring shape (`scripts/compile.ts:133`), the record schema, the Work projection (`standard/schema/work.ts:24-28`), the published context, the OpenAPI spec (`api/openapi.yaml:288`), eight documentation files, and — decisively — **the deterministic UUID seed**: `subject \n relation \n target.identifier` (`src/content/docs/standard/identifier-syntax.md:114-122`, ADR-0002). Changing a relation value re-mints every affected mapping IRI. Settling the two issues separately would mean two breaking spec bumps, two rounds of edits to the same paragraphs, two identifier churns, and an interim vocabulary in which one relation is SKOS and its sibling is not — precisely the incoherence #58 objects to.
+
+Timing forces the question now rather than later. Every record in the `data/` submodule is `status: draft` and the repository carries no tags. Under ADR-0004 a `draft` identifier carries no persistence promise and may be retracted without a tombstone, so re-minting mapping UUIDs today costs nothing. After the first promotion to `active` it costs a migration, permanently.
+
+## Decision drivers
+
+- A published relation must be semantically defensible against the domain and range its vocabulary actually declares — the objection in #58 is that `skos:exactMatch` fails this, not that it is imprecise.
+- The relation must not entail more than TextRefs can vouch for across independently modelled graphs. TextRefs asserts "same real-world referent, go here for more", not co-reference in all contexts.
+- One breaking change, not two. Both issues target `v0.2.0-draft` and touch the same enum, the same context block, the same UUID seed, and the same documentation paragraphs.
+- ADR-0002's seed shape (`subject`, `relation`, `target.identifier`) must survive unchanged; only the seed _values_ may move.
+- ADR-0004's draft-only registry makes identifier churn free exactly once. This decision has to land inside that window.
+- The vocabulary must be selectable by an author without judgement calls about confidence — see the decision below.
+
+## Considered options
+
+### For the Work ↔ external entity relation (#58)
+
+1. **Keep `skos:exactMatch`.** Zero work, and @julsraemy initially rated the domain/range problem cosmetic. Rejected: both issue participants ultimately agreed it is a real category error, not a typing artefact, and retaining it leaves the standard asserting a concept-mapping property between things that are not concepts.
+2. **`owl:sameAs`.** The de-facto LOD idiom for cross-KG entity links, as @stephenhart8 noted. Rejected: it entails indiscernibility — every assertion about one IRI holds for the other, in both directions — and its transitive closure imports whatever else the target has been fused with. TextRefs cannot guarantee that an external item sits at the same level of abstraction as its own Work.
+3. **`schema:sameAs`.** Widely deployed and JSON-LD-friendly (@maehr). Rejected: underspecified, so consumers read it differently, and its own definition is framed around a reference web page — which makes it a candidate for #59's relation, not this one.
+4. **Mint a TextRefs `equivalent` property**, following Linked Art's precedent (linked-art/linked.art#307). Semantically exact and fully under our control. Rejected for now on cost: `https://textrefs.org/ontology#` is declared in the context (`public/contexts/v1.jsonld:3`) but nothing is published there, so a minted term would be the first relation whose definition is undereferenceable and normatively load-bearing.
+5. **`prov:alternateOf`.** ← chosen
+
+### For the Work ↔ page-about-it relation (#59)
+
+1. **Keep `skos:closeMatch`.** Rejected: a Wikipedia article is not a weaker identifier for the work, it is a document about it. The relation is the wrong kind, not the wrong strength.
+2. **`dcterms:isReferencedBy`.** ← chosen
+3. **`schema:subjectOf` / `foaf:page`.** Viable but narrower; `dcterms:isReferencedBy` already has a sibling in the context (`superseded_by` → `dcterms:isReplacedBy`) and no competing reading.
+
+## Decision
+
+The relation vocabulary is selected by **what the target is**, never by how confident the author feels. Two values:
+
+| `relation` | Published as | Use when the target is |
+| ---------------- | ------------------------ | ------------------------------------- |
+| `alternateOf` | `prov:alternateOf` | another entity denoting the same work |
+| `isReferencedBy` | `dcterms:isReferencedBy` | a document or page _about_ the work |
+
+**`alternateOf` → `prov:alternateOf`** (#58). Per @maehr on the thread: "I guess `prov:alternateOf` is more commonly used, let's go with that", closing a discussion in which @julsraemy ("I'd be really happy with `prov:alternateOf` or `la:equivalent`") and @stephenhart8 ("both `prov:alternateOf` and `la:equivalent` seem to do the job") had already converged.
+
+`prov:alternateOf` relates two `prov:Entity` instances that present the same thing from different perspectives or at different levels of abstraction. Under PROV-CONSTRAINTS it is reflexive, symmetric and transitive — but, unlike `owl:sameAs`, **it does not entail indiscernibility**. Alternates may carry different attributes and sit at different levels of abstraction; that is what the property is _for_. This is the exact gap @julsraemy identified between what TextRefs wants to say and what `owl:sameAs` says, and it means the Wikidata-item objection (edition- and translation-level facts hanging off `Q35160`) does not bite: those facts are not licensed onto the TextRefs Work.
+
+**`isReferencedBy` → `dcterms:isReferencedBy`** (#59). The thread's one objection — that `dcterms:isReferencedBy` is "intended to be used with non-literal values" — only applies if the value is emitted as a string literal. Every mapping term in the published context already carries `"@type": "@id"`, which makes values node references, exactly as `superseded_by` → `dcterms:isReplacedBy` already does (`public/contexts/v1.jsonld:78-81`). The new term is declared the same way. Recorded here so the objection does not resurface.
+
+**`closeMatch` is removed entirely.** After the two reclassifications above, no SKOS mapping property is asserted with a `Work` as its subject — which is what #58's category argument actually requires. That leaves `closeMatch` with no valid use at all, because every one of its four documented jobs is either reassigned or unauthorable:
+
+- Work → proxy page (`data/AGENTS.md:14`, `mappings-and-resolver-targets.md:35`) → now `isReferencedBy`.
+- Work → whole edition, scan, or digital object, i.e. scope mismatch (`mappings-and-resolver-targets.md:119`) → a Work-subject SKOS assertion, so it falls to the same category objection. Such a target is either an alternate presentation of the work (`alternateOf`) or a resolver target, which is a different record type entirely; it is not a mapping.
+- Disputed authorship attribution (`authoring.md:126`, `data/AGENTS.md:41`) → `closeMatch` never expressed attribution uncertainty in the first place, and the guidance matches no record in the registry. Withdrawn without replacement; see follow-ups.
+- System ↔ system divergent versification (`specification.md:368`, `how-it-works.md:128`, `mappings-and-resolver-targets.md:117`) → the one case where `skos:closeMatch` is semantically defensible, since two citation systems genuinely are concept-scheme-like vocabularies of locators. But `MappingAssertion.subject` MUST be a Work IRI (`standard/schema/mapping-assertion.ts:6-11,20`), so this assertion cannot be authored at all. Specification §13 has been describing a record the schema forbids — a contradiction that predates this ADR.
+
+Retaining `closeMatch` for that last case was considered and rejected. It would leave a value in a published enum and in `api/openapi.yaml:288` that no valid record may carry, which is a trap for implementers reading the vocabulary as a menu. The asymmetry is decisive: **removing an enum value is breaking, adding one is not.** This change is already breaking, so the removal is free; re-introducing `closeMatch` once `subject` is widened to admit a CitationSystem IRI is an additive, non-breaking change at that point. Keeping it costs a permanently dead value; dropping it costs nothing.
+
+**Work projection.** The compiler's read-only projection onto `Work` becomes `alternateOf` and `isReferencedBy` (`standard/schema/work.ts:24-28`).
+
+**Identity.** ADR-0002's seed shape is unchanged, and the mapping namespace UUID `f16bb214-4241-549d-ad41-7b011f02befb` stays frozen (`identifier-syntax.md:106-112`). Only the seed _values_ change, re-minting all 24 existing mapping IRIs. This is deliberate and is why the change lands now, while ADR-0004 makes draft identifiers retractable without tombstones. Both axes ship as a single breaking change.
+
+**Versioning.** No spec version bump. The standard stays `v0.1.0-draft` (`ROADMAP.md:7`, `specification.md:9`) and carries `maturity: working-draft`, which `/standard/versioning/` defines as "Unstable. Data model and prose may change without notice and without a version bump while the core is settled." ADR-0004 was itself a breaking enum change and invoked exactly this clause to decline a bump; this ADR follows that precedent rather than inventing a `v0.2.0-draft` the repository has never been on. The issues' `v0.2.0-draft` label reflects their authors' expectation, not repository state. The **data package** is a separate artefact and does take a breaking SemVer bump under `/standard/versioning/#semver-rules-for-data-packages`, as ADR-0004's did.
+
+## Consequences
+
+### Positive
+
+- No relation in the vocabulary asserts a property outside its declared domain and range.
+- The vocabulary is chosen by target kind, so authoring needs no confidence judgement — the ambiguity that let one `closeMatch` value do four unrelated jobs is gone by construction.
+- `prov:alternateOf` gives cross-KG reconciliation without OWL identity: no indiscernibility, no imported `sameAs` closure.
+- One breaking change instead of two, with no interim state in which one relation is SKOS and its sibling is not.
+- `validate-data.ts` independently recomputes every mapping UUID from `[subject, relation, target.identifier]` (`scripts/validate-data.ts:71-75`), so the re-mint is verified by the existing build rather than by inspection.
+
+### Negative / trade-offs
+
+- All 24 existing mapping IRIs change. Any external reference to `https://textrefs.org/id/mapping/{uuid}` breaks. Permitted only because every record is `draft` under ADR-0004; this window does not reopen.
+- Breaking for the data package's SemVer under `/standard/versioning/#semver-rules-for-data-packages`, exercised under the pre-1.0 latitude ADR-0001 and ADR-0002 already established.
+- Specification §13's divergent-versification mechanism is left with no vocabulary at all until `subject` is widened. This makes an existing latent contradiction visible rather than creating a new one — the record §13 describes was never authorable — but the spec must now say so plainly instead of implying a usable relation.
+- The registry loses its documented mechanism for disputed attribution without gaining a replacement in this ADR.
+- `prov:alternateOf` is less immediately recognisable to consumers than `owl:sameAs`; naive `sameAs`-following clients will not traverse it. That is the intended behaviour, but it is a real interoperability cost.
+- A PROV vocabulary appears in the context solely for this one term, adding a namespace whose other machinery TextRefs does not use.
+
+### Follow-up actions
+
+Line anchors below are against `feat/preferred-citation-system` (PR [#63](https://github.com/textrefs/textrefs.org/pull/63)), which this work stacks on — see _Sequencing_ under Links.
+
+- [x] Replace the enum with `z.enum(['alternateOf', 'isReferencedBy'])` in `scripts/source-schema.ts:73` and `standard/schema/mapping-assertion.ts:21`, keeping the two in lockstep. ADR-0005's Zod `MappingSource` makes an unknown relation fail at parse time; before it, the authoring shape was an unvalidated TypeScript type.
+- [x] Replace the hard-coded two-way branch in `scripts/compile.ts:447-455,474-475` with an enum-keyed accumulator, so a further relation needs no new branch; update the `// Direct SKOS mapping edges` comment at `:447`, which no longer describes the projection at all.
+- [x] Add `"prov": "http://www.w3.org/ns/prov#"` to the namespace block in `public/contexts/v1.jsonld:2-7`; replace the `exactMatch` and `closeMatch` terms with `alternateOf` → `prov:alternateOf` and `isReferencedBy` → `dcterms:isReferencedBy`, both `"@type": "@id"`.
+- [x] Rename the projection fields in `standard/schema/work.ts:33-34` and rewrite the comment at `:31`, which names SKOS explicitly.
+- [x] Reclassify all 12 Wikidata mappings to `alternateOf` and all 12 Wikipedia mappings to `isReferencedBy` in `data/works/*.yaml`, via a PR to `textrefs/registry`; bump each `modified`.
+- [ ] Confirm in the spec that `isReferencedBy` targets remain lookup aliases (`scripts/compile.ts:518`, `setAlias(aliases, mapping.identifier, workIri)`). Recommended: yes — the alias table is a lookup convenience, not an identity claim — but it must be stated rather than inherited.
+- [x] Sweep the prose: `specification.md:131,140,247,264,373`, `json-ld.md:31-36,72-73`, `identifier-syntax.md:122` (the seed's enumerated literal values), `mappings-and-resolver-targets.md:35,41,117,119,130`, `authoring.md:38,126`, `how-it-works.md:97,128`, `data/AGENTS.md:14,41`, and `api/openapi.yaml:220-230,297`.
+- [x] Decide what the re-mint checklists mean now: `README.md:135`, `.github/PULL_REQUEST_TEMPLATE.md:39` and `data/.github/PULL_REQUEST_TEMPLATE.md:19` link a withdrawn record to its successor with `relation: exactMatch`. `alternateOf` fits (same referent, different presentation), but `superseded_by` → `dcterms:isReplacedBy` already covers succession — this may be a redundant instruction rather than one to translate.
+- [x] Extend `scripts/compile.test.ts` (260 lines from ADR-0005, no mapping coverage): projection grouping, tombstone exclusion, `mappingUuid` stability for a fixed triple, and rejection of an out-of-enum relation. Add an `isReferencedBy` mapping alongside `src/lib/registry.fixture.ts:93`.
+- [ ] **Separate ADR:** widen `MappingAssertion.subject` to admit a CitationSystem IRI, then re-introduce `closeMatch` as an additive change. Until then specification §13 must state that the equivalence it describes is not yet expressible.
+- [ ] **Separate ADR or issue:** how to express disputed authorship attribution, now that `closeMatch` no longer pretends to.
+- [ ] Publish an ontology stub at `https://textrefs.org/ontology#`. Pre-existing debt (`tr:Work`, `tr:relation`, `tr:locator` are all undereferenceable), not created here, but option 4 above was rejected partly on its account.
+
+## Links
+
+- Related ADRs: ADR-0002 (UUID seed — the reason a relation change re-mints identifiers), ADR-0004 (draft-only lifecycle — the reason the re-mint is free now, and the precedent for a breaking change without a spec version bump), ADR-0005 (introduces the Zod authoring schema this change extends)
+- Related issues / PRs: textrefs/textrefs.org#58, textrefs/textrefs.org#59
+- **Sequencing:** this ADR and its implementation stack on PR [#63](https://github.com/textrefs/textrefs.org/pull/63) (ADR-0005, `feat/preferred-citation-system` → `staging`), not directly on `staging`. #63 introduces `scripts/source-schema.ts` — the runtime validation this change's enum needs — and `scripts/compile.test.ts`, where the missing mapping coverage belongs. Basing on `staging` instead would mean editing an unvalidated TypeScript type and then resolving the same conflict when #63 merges. Retarget this PR's base to `staging` once #63 lands.
+- External references: [PROV-O `alternateOf`](https://www.w3.org/TR/prov-o/#alternateOf), [PROV-CONSTRAINTS §alternate](https://www.w3.org/TR/prov-constraints/#term-alternate), [DCMI Metadata Terms `isReferencedBy`](https://www.dublincore.org/specifications/dublin-core/dcmi-terms/#http://purl.org/dc/terms/isReferencedBy), [SKOS mapping properties](https://www.w3.org/TR/skos-reference/#mapping), linked-art/linked.art#307
diff --git a/public/contexts/v1.jsonld b/public/contexts/v1.jsonld
index de2af1e..7884ea3 100644
--- a/public/contexts/v1.jsonld
+++ b/public/contexts/v1.jsonld
@@ -3,6 +3,7 @@
"tr": "https://textrefs.org/ontology#",
"skos": "http://www.w3.org/2004/02/skos/core#",
"dcterms": "http://purl.org/dc/terms/",
+ "prov": "http://www.w3.org/ns/prov#",
"schema": "https://schema.org/",
"xsd": "http://www.w3.org/2001/XMLSchema#",
"id": "@id",
@@ -38,12 +39,12 @@
"@type": "xsd:date"
},
"relation": "tr:relation",
- "exactMatch": {
- "@id": "skos:exactMatch",
+ "alternateOf": {
+ "@id": "prov:alternateOf",
"@type": "@id"
},
- "closeMatch": {
- "@id": "skos:closeMatch",
+ "isReferencedBy": {
+ "@id": "dcterms:isReferencedBy",
"@type": "@id"
},
"subject": {
diff --git a/scripts/compile.test.ts b/scripts/compile.test.ts
index 8228c71..8de451b 100644
--- a/scripts/compile.test.ts
+++ b/scripts/compile.test.ts
@@ -9,6 +9,7 @@ import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
+import { v5 as uuidv5 } from 'uuid';
import { compileRegistry, type CompiledRegistry } from './compile.js';
type RegistryFiles = {
@@ -258,3 +259,112 @@ references:
assert.equal(work.preferred_citation_system_key, 'primary-section');
}
});
+
+// --- Mapping relation vocabulary (ADR-0006) -------------------------------
+
+const mappings = `
+mappings:
+ - relation: alternateOf
+ identifier: 'https://www.wikidata.org/entity/Q1'
+ source: manual-curation
+ status: active
+ created: 2026-01-01
+ modified: 2026-01-01
+ - relation: isReferencedBy
+ identifier: 'https://en.wikipedia.org/wiki/Test'
+ source: manual-curation
+ status: active
+ created: 2026-01-01
+ modified: 2026-01-01
+`;
+
+const workWithMappings = (extra = '') => ({
+ systems: twoSystems,
+ works: {
+ 'test.work': `${workHeader()}
+citation_system: primary-section
+references:
+ - '5'
+${mappings}${extra}`,
+ },
+});
+
+test('the work projection groups every relation onto its own array', () => {
+ const reg = compileFixture(workWithMappings());
+ const work = reg.works.find((w) => w.key === 'test.work');
+ assert.ok(work);
+ assert.deepEqual(work.alternateOf, ['https://www.wikidata.org/entity/Q1']);
+ assert.deepEqual(work.isReferencedBy, ['https://en.wikipedia.org/wiki/Test']);
+ assert.equal(reg.mappings.length, 2);
+});
+
+test('tombstoned mappings are excluded from the projection', () => {
+ const reg = compileFixture({
+ systems: twoSystems,
+ works: {
+ 'test.work': `${workHeader()}
+citation_system: primary-section
+references:
+ - '5'
+
+mappings:
+ - relation: alternateOf
+ identifier: 'https://www.wikidata.org/entity/Q1'
+ source: manual-curation
+ status: withdrawn
+ created: 2026-01-01
+ modified: 2026-01-01
+ - relation: isReferencedBy
+ identifier: 'https://en.wikipedia.org/wiki/Test'
+ source: manual-curation
+ status: blocked
+ created: 2026-01-01
+ modified: 2026-01-01
+`,
+ },
+ });
+ const work = reg.works.find((w) => w.key === 'test.work');
+ assert.ok(work);
+ assert.equal(work.alternateOf, undefined);
+ assert.equal(work.isReferencedBy, undefined);
+ // The reified assertions survive as tombstones; only the projection drops them.
+ assert.equal(reg.mappings.length, 2);
+});
+
+test('mapping IRIs are deterministic from [subject, relation, target]', () => {
+ const reg = compileFixture(workWithMappings());
+ // Recomputed independently here, exactly as validate-data.ts and any
+ // third-party implementation would (identifier-syntax, MappingAssertion seed).
+ for (const m of reg.mappings) {
+ const seed = [m.subject, m.relation, m.target.identifier].join('\n');
+ assert.equal(
+ m.id,
+ `https://textrefs.org/id/mapping/${uuidv5(seed, 'f16bb214-4241-549d-ad41-7b011f02befb')}`,
+ );
+ }
+ // The relation is *in* the seed, so reclassifying a target re-mints its IRI.
+ const [alternate, referenced] = reg.mappings;
+ assert.notEqual(alternate.id, referenced.id);
+});
+
+test('a relation outside the enum is rejected at parse time', (t) => {
+ const message = expectCompileError(t, {
+ systems: twoSystems,
+ works: {
+ 'test.work': `${workHeader()}
+citation_system: primary-section
+references:
+ - '5'
+
+mappings:
+ - relation: closeMatch
+ identifier: 'https://en.wikipedia.org/wiki/Test'
+ source: manual-curation
+ status: active
+ created: 2026-01-01
+ modified: 2026-01-01
+`,
+ },
+ });
+ assert.match(message, /test\.work/);
+});
diff --git a/scripts/compile.ts b/scripts/compile.ts
index 0ed0e1f..884bd76 100644
--- a/scripts/compile.ts
+++ b/scripts/compile.ts
@@ -20,6 +20,7 @@ import {
parseSource,
SystemSource,
WorkSource,
+ type MappingSource,
type ReferenceRangeSource as ReferenceRange,
type ReferenceSource,
type ResolverEntrySource as ResolverEntry,
@@ -444,16 +445,17 @@ export function compileRegistry(dataRootOverride?: string): CompiledRegistry {
const workIri = `https://textrefs.org/id/work/${workKey}`;
const systemKey = src.citation_system;
- // Direct SKOS mapping edges (skos:exactMatch / skos:closeMatch via the
- // context) projected from the work's mapping assertions, in addition to
- // the reified MappingAssertion records below.
- const exactMatches: string[] = [];
- const closeMatches: string[] = [];
+ // Direct mapping edges (prov:alternateOf / dcterms:isReferencedBy via
+ // the context, ADR-0006) projected from the work's mapping assertions,
+ // in addition to the reified MappingAssertion records below. Keyed off
+ // the relation enum so adding a relation needs no branch here.
+ const mappingEdges: Record = {
+ alternateOf: [],
+ isReferencedBy: [],
+ };
for (const m of src.mappings ?? []) {
if (TOMBSTONE_STATUSES.has(m.status)) continue;
- (m.relation === 'exactMatch' ? exactMatches : closeMatches).push(
- m.identifier,
- );
+ mappingEdges[m.relation].push(m.identifier);
}
const workRecord = {
@@ -471,8 +473,9 @@ export function compileRegistry(dataRootOverride?: string): CompiledRegistry {
? { superseded_by: src.work.superseded_by }
: {}),
...(src.work.creators ? { creators: src.work.creators } : {}),
- ...(exactMatches.length ? { exactMatch: exactMatches } : {}),
- ...(closeMatches.length ? { closeMatch: closeMatches } : {}),
+ ...Object.fromEntries(
+ Object.entries(mappingEdges).filter(([, targets]) => targets.length),
+ ),
};
const workParsed = Work.safeParse(workRecord);
if (!workParsed.success) {
@@ -515,6 +518,9 @@ export function compileRegistry(dataRootOverride?: string): CompiledRegistry {
throw new Error(`invalid mapping: ${uuid}`);
}
outMappings.push(parsed.data);
+ // Deliberate under ADR-0006: an `isReferencedBy` target (a page
+ // *about* the work) stays a lookup alias for it. The alias table is
+ // a lookup convenience, not an identity claim.
setAlias(aliases, mapping.identifier, workIri);
}
diff --git a/scripts/source-schema.ts b/scripts/source-schema.ts
index 2660b31..c5b01c3 100644
--- a/scripts/source-schema.ts
+++ b/scripts/source-schema.ts
@@ -70,7 +70,10 @@ export const ReferenceRangeSource = z.discriminatedUnion('kind', [
export type ReferenceRangeSource = z.infer;
export const MappingSource = z.strictObject({
- relation: z.enum(['exactMatch', 'closeMatch']),
+ // ADR-0006: chosen by what the target *is*, never by confidence.
+ // alternateOf — another entity denoting the same work.
+ // isReferencedBy — a document or page about the work.
+ relation: z.enum(['alternateOf', 'isReferencedBy']),
identifier: z.string().min(1),
conforms_to: z
.union([z.string().min(1), z.array(z.string().min(1))])
diff --git a/src/content/docs/get-started/authoring.md b/src/content/docs/get-started/authoring.md
index 31a9101..e55b7b8 100644
--- a/src/content/docs/get-started/authoring.md
+++ b/src/content/docs/get-started/authoring.md
@@ -35,7 +35,7 @@ work:
citation_system: dhammapada-chapter-verse # the work's PREFERRED citation system
mappings:
- - relation: exactMatch
+ - relation: alternateOf
identifier: 'https://www.wikidata.org/entity/Q220114'
conforms_to: 'https://www.wikidata.org/'
source: manual-curation
@@ -123,7 +123,7 @@ Follow CSL-JSON conventions so citeproc-js / Zotero render correctly.
- Mononyms (Homer, Plato, Confucius, Laozi, Murasaki Shikibu, …): `kind: person` with `family` only and no `given`. CSL convention for single-name authors; matches Chicago's "Homer, _Iliad_ 1.1." output.
- Anonymous / collective: **omit `creators` entirely**. Don't write a literal "Anonymous" — absence is the correct CSL signal.
- Reserve `kind: literal` for names that genuinely should not decompose: corporate/institutional authors ("World Health Organization") or pseudonymous attribution strings ("[Pseudo-]Aristotle").
-- Attributed-but-disputed (e.g. Laozi for _Daodejing_): record the traditional attribution as `kind: person, family: Laozi`; encode uncertainty via a `closeMatch` mapping, not in the name string.
+- Attributed-but-disputed (e.g. Laozi for _Daodejing_): record the traditional attribution as `kind: person, family: Laozi`; do not encode the dispute in the name string. TextRefs has no relation for attribution uncertainty in this version.
## How URL templates work
diff --git a/src/content/docs/get-started/how-it-works.md b/src/content/docs/get-started/how-it-works.md
index 73275f0..5aecfc0 100644
--- a/src/content/docs/get-started/how-it-works.md
+++ b/src/content/docs/get-started/how-it-works.md
@@ -94,7 +94,7 @@ Work-level equivalences live in a `MappingAssertion`:
{
"type": "MappingAssertion",
"subject": "https://textrefs.org/id/work/plato.republic",
- "relation": "exactMatch",
+ "relation": "alternateOf",
"target": {
"identifier": "https://www.wikidata.org/entity/Q123397",
"conforms_to": "https://www.wikidata.org/"
@@ -125,7 +125,7 @@ An English translation, a German translation, a Greek edition, and a library sca
For complete worked examples, see the live [Dhammapada work page](/id/work/dhammapada/) (four providers, two languages, 423 references) or the [Plato _Republic_ work page](/id/work/plato.republic/) (Stephanus pagination). The contributor YAML behind them is documented in [Authoring registry data](/get-started/authoring/).
-Where traditions number passages differently, create separate references under separate citation systems and connect them with `closeMatch` mappings. Do not collapse divergent versification, pagination, or segmentation into one identity.
+Where traditions number passages differently, create separate references under separate citation systems. Do not collapse divergent versification, pagination, or segmentation into one identity. The equivalence between the citation systems themselves is not yet expressible in this version: `MappingAssertion.subject` MUST be a Work IRI, so a system-to-system assertion cannot be authored. A future revision may widen `subject` to admit a `CitationSystem` IRI.
## What TextRefs does not store
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 eaa3ac3..134b31d 100644
--- a/src/content/docs/get-started/mappings-and-resolver-targets.md
+++ b/src/content/docs/get-started/mappings-and-resolver-targets.md
@@ -32,13 +32,13 @@ Common mapping targets include:
- DOIs, Handles, ARKs, PURLs, or URN:NBNs for editions or digital objects;
- another TextRefs Work when two registries need to be aligned.
-Use `exactMatch` only when the target identifies the same work with enough precision. Use `closeMatch` for edition or scope mismatches.
+Choose the relation by what the target is, not by how confident you feel: use `alternateOf` when the target is another entity denoting the same work (e.g. a Wikidata item). Use `isReferencedBy` when the target is a document or page about the work (e.g. a Wikipedia article).
```json
{
"type": "MappingAssertion",
"subject": "https://textrefs.org/id/work/dhammapada",
- "relation": "exactMatch",
+ "relation": "alternateOf",
"target": {
"identifier": "https://www.wikidata.org/entity/Q220114",
"conforms_to": "https://www.wikidata.org/"
@@ -114,9 +114,9 @@ An author's name alone is not a `Work`. For example, "Confucius" is an authority
**Translations.** A translation is a resolver target when it lets readers inspect the cited passage. It is not a new canonical reference unless the translation has its own independently cited segmentation.
-**Divergent numbering.** If two traditions number the same material differently, create separate `CanonicalReference`s under separate `CitationSystem`s and connect them with `closeMatch` mappings.
+**Divergent numbering.** If two traditions number the same material differently, create separate `CanonicalReference`s under separate `CitationSystem`s. The equivalence between the two citation systems is not yet expressible in this version: `MappingAssertion.subject` MUST be a Work IRI, so a system-to-system assertion cannot be authored. A future revision may widen `subject` to admit a `CitationSystem` IRI.
-**Contained-by relationships.** If an identifier points to a whole edition, scan, or digital object rather than the exact passage, avoid `exactMatch`. Use `closeMatch` only when the connection is useful and the scope is clear.
+**Contained-by relationships.** If an identifier points to a whole edition, scan, or digital object rather than the exact passage, it is not a `MappingAssertion` target at all. If it is an alternate presentation of the whole work, model it as `alternateOf`; otherwise it belongs in `resolver_targets`.
**Unstable websites.** A website URL can be useful as a resolver target even if it is not a stable identifier. Do not derive TextRefs IDs from it.
@@ -127,7 +127,7 @@ Before proposing mappings or resolver targets, check that:
- 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 `relation` matches what the target actually is (`alternateOf` for a same-work entity, `isReferencedBy` for a document about the work);
- each `resolver_targets` entry has a dereferenceable URL and honest access metadata;
- the proposal documents its source or curation basis;
- no full text, translation text, apparatus, or commentary is copied into the registry.
diff --git a/src/content/docs/standard/identifier-syntax.md b/src/content/docs/standard/identifier-syntax.md
index 6d7c969..d75f46c 100644
--- a/src/content/docs/standard/identifier-syntax.md
+++ b/src/content/docs/standard/identifier-syntax.md
@@ -119,7 +119,7 @@ relation
target.identifier
```
-`subject` MUST be the canonical Work IRI (`https://textrefs.org/id/work/{work_key}`). `relation` MUST be the literal string `exactMatch` or `closeMatch`. `target.identifier` MUST be used as supplied by the source record, after any IRI normalization the source profile already applies. `target.conforms_to` is informative and does NOT enter the seed.
+`subject` MUST be the canonical Work IRI (`https://textrefs.org/id/work/{work_key}`). `relation` MUST be the literal string `alternateOf` or `isReferencedBy`. `target.identifier` MUST be used as supplied by the source record, after any IRI normalization the source profile already applies. `target.conforms_to` is informative and does NOT enter the seed.
The canonical URI is `https://textrefs.org/id/mapping/{uuid}`.
diff --git a/src/content/docs/standard/json-ld.md b/src/content/docs/standard/json-ld.md
index ba46fbe..01b2f8a 100644
--- a/src/content/docs/standard/json-ld.md
+++ b/src/content/docs/standard/json-ld.md
@@ -6,7 +6,7 @@ sidebar:
order: 5
---
-TextRefs records are plain JSON that becomes linked data through a published JSON-LD context. The context maps TextRefs terms onto a small TextRefs ontology namespace (`tr:`) plus established vocabularies — SKOS for labels, schemes, and mapping relations, Dublin Core Terms for dates and provenance, schema.org for URLs and providers, and XSD for date typing.
+TextRefs records are plain JSON that becomes linked data through a published JSON-LD context. The context maps TextRefs terms onto a small TextRefs ontology namespace (`tr:`) plus established vocabularies — SKOS for labels and schemes, PROV-O and Dublin Core Terms for mapping relations, Dublin Core Terms also for dates and provenance, schema.org for URLs and providers, and XSD for date typing.
The `v1` context is served at:
@@ -16,24 +16,25 @@ https://textrefs.org/contexts/v1.jsonld
## Vocabularies
-| Prefix | Namespace | Used for |
-| --------- | -------------------------------------- | -------------------------------------------------------------------------- |
-| `tr` | `https://textrefs.org/ontology#` | TextRefs object types, keys, and TextRefs-specific metadata |
-| `skos` | `http://www.w3.org/2004/02/skos/core#` | Labels, schemes (`inScheme`), and mapping relations |
-| `dcterms` | `http://purl.org/dc/terms/` | `created`, `modified`, `source`, `language`, `license`, `conformsTo` |
-| `schema` | `https://schema.org/` | `url`, `provider`, `edition`, `creator`, `familyName`, `givenName`, `name` |
-| `xsd` | `http://www.w3.org/2001/XMLSchema#` | `xsd:date` typing for `created` / `modified` / `last_checked` |
+| Prefix | Namespace | Used for |
+| --------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------- |
+| `tr` | `https://textrefs.org/ontology#` | TextRefs object types, keys, and TextRefs-specific metadata |
+| `skos` | `http://www.w3.org/2004/02/skos/core#` | Labels and schemes (`inScheme`) |
+| `dcterms` | `http://purl.org/dc/terms/` | `created`, `modified`, `source`, `language`, `license`, `conformsTo`, `isReferencedBy` mapping relation |
+| `prov` | `http://www.w3.org/ns/prov#` | Work ↔ same-entity mapping relation (`alternateOf`) |
+| `schema` | `https://schema.org/` | `url`, `provider`, `edition`, `creator`, `familyName`, `givenName`, `name` |
+| `xsd` | `http://www.w3.org/2001/XMLSchema#` | `xsd:date` typing for `created` / `modified` / `last_checked` |
## Mapping relations
-The MVP mapping relations map directly onto SKOS:
+The mapping relations are chosen by what the target is, never by author confidence:
-- `exactMatch` → `skos:exactMatch`
-- `closeMatch` → `skos:closeMatch`
+- `alternateOf` → `prov:alternateOf`, when the target is another entity denoting the same work (e.g. a Wikidata item).
+- `isReferencedBy` → `dcterms:isReferencedBy`, when the target is a document or page about the work (e.g. a Wikipedia article).
-Published `Work` records additionally carry direct `exactMatch` / `closeMatch` arrays derived from their non-tombstoned 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. The arrays are a read-only projection; the assertion stays authoritative ([Specification §6](/standard/specification/#6-work)).
+Published `Work` records additionally carry direct `alternateOf` / `isReferencedBy` arrays derived from their non-tombstoned mapping assertions, published as `prov:alternateOf` / `dcterms:isReferencedBy` edges from the work IRI to the mapped identifiers without dereferencing the reified `MappingAssertion` records. The arrays are a read-only projection; the assertion stays authoritative ([Specification §6](/standard/specification/#6-work)).
-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).
+Choose `alternateOf` when the target identifies the same work from a different perspective or at a different level of abstraction; choose `isReferencedBy` when the target describes the work rather than identifying it. See [Specification §10](/standard/specification/#10-mappingassertion).
## The context document
@@ -43,6 +44,7 @@ Use `exactMatch` only when the mapped object identifies the same reference with
"tr": "https://textrefs.org/ontology#",
"skos": "http://www.w3.org/2004/02/skos/core#",
"dcterms": "http://purl.org/dc/terms/",
+ "prov": "http://www.w3.org/ns/prov#",
"schema": "https://schema.org/",
"xsd": "http://www.w3.org/2001/XMLSchema#",
"id": "@id",
@@ -69,8 +71,8 @@ Use `exactMatch` only when the mapped object identifies the same reference with
"created": { "@id": "dcterms:created", "@type": "xsd:date" },
"modified": { "@id": "dcterms:modified", "@type": "xsd:date" },
"relation": "tr:relation",
- "exactMatch": { "@id": "skos:exactMatch", "@type": "@id" },
- "closeMatch": { "@id": "skos:closeMatch", "@type": "@id" },
+ "alternateOf": { "@id": "prov:alternateOf", "@type": "@id" },
+ "isReferencedBy": { "@id": "dcterms:isReferencedBy", "@type": "@id" },
"subject": { "@id": "tr:subject", "@type": "@id" },
"target": "tr:target",
"identifier": { "@id": "tr:identifier", "@type": "@id" },
diff --git a/src/content/docs/standard/specification.md b/src/content/docs/standard/specification.md
index fbd3a04..f1f3285 100644
--- a/src/content/docs/standard/specification.md
+++ b/src/content/docs/standard/specification.md
@@ -128,7 +128,7 @@ A `Work.key` is a single flat registry key used to identify the abstract work in
}
```
-Required: `id`, `key`, `type` (`Work`), `preferred_label`, `preferred_citation_system_key`, `status`, plus administrative metadata ([§12](#12-administrative-metadata)). Optional: `creators`, `exactMatch`, `closeMatch`. The `id` MUST be a persistent TextRefs HTTP URI of the form `https://textrefs.org/id/work/{key}`, where `{key}` is one flat key and occupies exactly one URI path segment. The `key` MUST be stable and suitable for deterministic identity generation.
+Required: `id`, `key`, `type` (`Work`), `preferred_label`, `preferred_citation_system_key`, `status`, plus administrative metadata ([§12](#12-administrative-metadata)). Optional: `creators`, `alternateOf`, `isReferencedBy`. The `id` MUST be a persistent TextRefs HTTP URI of the form `https://textrefs.org/id/work/{key}`, where `{key}` is one flat key and occupies exactly one URI path segment. The `key` MUST be stable and suitable for deterministic identity generation.
- `preferred_citation_system_key` MUST reference a known `CitationSystem`. It governs the bare `/cite/{work_key}/{locator}/` alias and default presentation only; it is identity-neutral and MUST NOT affect the validation or resolution of a fully qualified reference ([§7](#7-citationsystem)).
- A `Work` MAY be referenced under more than one `CitationSystem`, of which exactly one is preferred.
@@ -137,7 +137,7 @@ Required: `id`, `key`, `type` (`Work`), `preferred_label`, `preferred_citation_s
External identifiers for a `Work` (e.g. Wikidata Q-ID, DOI, VIAF) are asserted as `MappingAssertion`s whose `subject` is the `Work` ([§10](#10-mappingassertion)). They MUST NOT be authored directly on the `Work`.
-`exactMatch` and `closeMatch` are the compiler's read-only projection of those assertions — every one whose `status` is not `withdrawn` or `blocked`, grouped by `relation` — so SKOS-aware consumers get `skos:exactMatch` / `skos:closeMatch` edges straight from the work IRI ([JSON-LD](/standard/json-ld/#mapping-relations)). They carry no status or provenance; the `MappingAssertion` stays authoritative.
+`alternateOf` and `isReferencedBy` are the compiler's read-only projection of those assertions — every one whose `status` is not `withdrawn` or `blocked`, grouped by `relation` — published straight from the work IRI as `prov:alternateOf` / `dcterms:isReferencedBy` edges ([JSON-LD](/standard/json-ld/#mapping-relations)). They carry no status or provenance; the `MappingAssertion` stays authoritative.
## 7. CitationSystem
@@ -244,7 +244,7 @@ A `MappingAssertion` records a curated equivalence claim between a TextRefs `Wor
"id": "https://textrefs.org/id/mapping/{uuid}",
"type": "MappingAssertion",
"subject": "https://textrefs.org/id/work/new-testament",
- "relation": "exactMatch",
+ "relation": "alternateOf",
"target": {
"identifier": "https://www.wikidata.org/entity/Q18813",
"conforms_to": "https://www.wikidata.org/"
@@ -261,7 +261,7 @@ Required: `id`, `type` (`MappingAssertion`), `subject`, `relation`, `target`, `s
- `subject` MUST be a `Work` IRI of the form `https://textrefs.org/id/work/{work_key}`. Per-passage external identifiers (e.g. the CTS URN of a single verse) are derived from work-level mappings combined with the reference locator at resolve time; they MUST NOT be stored as separate `MappingAssertion` records.
- `target.identifier` MUST be an IRI ([RFC 3987](https://www.rfc-editor.org/rfc/rfc3987)) that identifies a **textual resource**: a work, edition, manuscript, citation system, or another TextRefs `Work`.
- `target.conforms_to` is OPTIONAL. When present, it MUST be a dereferenceable IRI — or an array of such IRIs — identifying the specification or identifier scheme to which `target.identifier` conforms (e.g. the home page of the CTS specification, the Wikidata project, or a DOI handbook section). It is informative: validators MUST NOT key behaviour off it; the IRI in `identifier` is authoritative. See [Appendix B](#appendix-b-well-known-external-identifier-schemes-informative) for non-normative examples.
-- `relation` MUST be one of the SKOS-compatible values `exactMatch` or `closeMatch`. Use `exactMatch` only when the mapped resource identifies the same work with sufficient precision; if there is any uncertainty about edition, coverage, or work boundaries, use `closeMatch`.
+- `relation` MUST be chosen by what `target.identifier` denotes, never by author confidence: use `alternateOf` when the target is another entity denoting the same work (e.g. a Wikidata item); use `isReferencedBy` when the target is a document or page about the work (e.g. a Wikipedia article).
- `source` documents the basis for the assertion. A structured [W3C PROV-O](https://www.w3.org/TR/prov-o/) mapping is reserved for a future version.
## 11. Identifier policy
@@ -370,7 +370,7 @@ This is the case that motivates separating identity from location. The New Testa
Adding another edition or translation appends one entry to `resolver_targets`. The reference identity — its UUID, its work, its citation system, its locator — does not change.
-**Divergent versification** is the one case that _does_ create separate references. Where traditions number verses differently (e.g. the Psalms in the Masoretic text versus the Vulgate/Septuagint), each tradition is a distinct `CitationSystem`, its references are distinct `CanonicalReference`s, and the equivalence between them is recorded as a `closeMatch` `MappingAssertion` — not by collapsing them into one identity.
+**Divergent versification** is the one case that _does_ create separate references. Where traditions number verses differently (e.g. the Psalms in the Masoretic text versus the Vulgate/Septuagint), each tradition is a distinct `CitationSystem`, its references are distinct `CanonicalReference`s — not collapsed into one identity. The equivalence between citation systems is not yet expressible in this version: `MappingAssertion.subject` MUST be a `Work` IRI ([§10](#10-mappingassertion)), so a system-to-system assertion cannot be authored. A future revision may widen `subject` to admit a `CitationSystem` IRI.
## 14. Validation requirements
diff --git a/src/lib/registry.fixture.ts b/src/lib/registry.fixture.ts
index e359d5c..29cd87a 100644
--- a/src/lib/registry.fixture.ts
+++ b/src/lib/registry.fixture.ts
@@ -19,6 +19,8 @@ export const fixtureRegistry: CompiledRegistry = {
status: 'active',
created: '2026-01-01',
modified: '2026-01-01',
+ alternateOf: ['https://example.org/fixture-work'],
+ isReferencedBy: ['https://example.org/about-fixture-work'],
},
],
systems: [
@@ -90,7 +92,7 @@ export const fixtureRegistry: CompiledRegistry = {
id: 'https://textrefs.org/id/mapping/00000000-0000-5000-8000-000000000002',
type: 'MappingAssertion',
subject: fixtureWorkIri,
- relation: 'exactMatch',
+ relation: 'alternateOf',
target: {
identifier: 'https://example.org/fixture-work',
},
@@ -99,12 +101,29 @@ export const fixtureRegistry: CompiledRegistry = {
created: '2026-01-01',
modified: '2026-01-01',
},
+ // The second relation ADR-0006 defines: a page *about* the work rather
+ // than another identifier for it. Present so the work page renders both
+ // relation tags under `build:fast`.
+ {
+ id: 'https://textrefs.org/id/mapping/00000000-0000-5000-8000-000000000004',
+ type: 'MappingAssertion',
+ subject: fixtureWorkIri,
+ relation: 'isReferencedBy',
+ target: {
+ identifier: 'https://example.org/about-fixture-work',
+ },
+ source: 'Local fixture for fast site validation.',
+ status: 'active',
+ created: '2026-01-01',
+ modified: '2026-01-01',
+ },
],
aliases: {
'fixture.work/fixture-section/1': fixtureRefIri,
'fixture.work/1': fixtureRefIri,
'fixture.work/fixture-alternate/1': fixtureAltRefIri,
'https://example.org/fixture-work': fixtureWorkIri,
+ 'https://example.org/about-fixture-work': fixtureWorkIri,
},
warnings: 0,
};
diff --git a/standard/schema/mapping-assertion.ts b/standard/schema/mapping-assertion.ts
index aebb7be..13709c1 100644
--- a/standard/schema/mapping-assertion.ts
+++ b/standard/schema/mapping-assertion.ts
@@ -18,7 +18,8 @@ export const MappingAssertionBase = AdminMetadata.extend({
),
type: z.literal('MappingAssertion'),
subject: WorkIri,
- relation: z.enum(['exactMatch', 'closeMatch']),
+ // ADR-0006. Kept in lockstep with MappingSource in scripts/source-schema.ts.
+ relation: z.enum(['alternateOf', 'isReferencedBy']),
target: z.object({
identifier: Iri,
conforms_to: z.union([Iri, z.array(Iri).min(1)]).optional(),
diff --git a/standard/schema/work.ts b/standard/schema/work.ts
index e912992..3517ad5 100644
--- a/standard/schema/work.ts
+++ b/standard/schema/work.ts
@@ -28,10 +28,10 @@ export const WorkBase = AdminMetadata.extend({
preferred_citation_system_key: FlatKey,
creators: z.array(Creator).optional(),
// Compiler-derived projection of the work's non-tombstoned
- // MappingAssertions (skos:exactMatch / skos:closeMatch in the published
- // context). Never authored.
- exactMatch: z.array(Iri).optional(),
- closeMatch: z.array(Iri).optional(),
+ // MappingAssertions (prov:alternateOf / dcterms:isReferencedBy in the
+ // published context, ADR-0006). Never authored.
+ alternateOf: z.array(Iri).optional(),
+ isReferencedBy: z.array(Iri).optional(),
});
export const Work = WorkBase.superRefine((w, ctx) => {
From 0e9c8240fa479dd3f5973db8a9614f9675495e80 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
<14755525+maehr@users.noreply.github.com>
Date: Wed, 12 Aug 2026 22:19:11 +0200
Subject: [PATCH 32/45] chore(data): bump registry pin to the ADR-0006
reclassification
---
data | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/data b/data
index c0a3275..0f00bd4 160000
--- a/data
+++ b/data
@@ -1 +1 @@
-Subproject commit c0a3275ad21f76c68c7a375819f8833681f0e87e
+Subproject commit 0f00bd4be5ed7039391ee3fce7c6e8c537d0a397
From aba818dbc8a39922069f55c7e668d0cd82838b07 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
Date: Wed, 12 Aug 2026 22:32:10 +0200
Subject: [PATCH 33/45] feat(compile): map locator variables into a provider's
own vocabulary (#71) (#72)
---
scripts/compile.test.ts | 172 ++++++++++++++++++
scripts/compile.ts | 43 ++++-
scripts/source-schema.ts | 22 +++
src/content/docs/get-started/authoring.md | 49 +++--
src/content/docs/get-started/index.md | 2 +-
.../mappings-and-resolver-targets.md | 15 +-
6 files changed, 279 insertions(+), 24 deletions(-)
diff --git a/scripts/compile.test.ts b/scripts/compile.test.ts
index 8de451b..eb7e7f2 100644
--- a/scripts/compile.test.ts
+++ b/scripts/compile.test.ts
@@ -368,3 +368,175 @@ mappings:
});
assert.match(message, /test\.work/);
});
+
+// --- Resolver URL expansion -------------------------------------------------
+//
+// `vars` (this section's main subject) rides on the same skip-and-warn path as
+// a missing template variable, so the pre-existing `url` / `url_by` behaviour
+// is pinned here too — it had no coverage before.
+
+const bookChapter = {
+ 'book-chapter': system(
+ 'book-chapter',
+ 'active',
+ '^(?[1-4]?[A-Za-z]+)\\.(?[1-9][0-9]*)$',
+ ),
+};
+
+/** One work, one resolver, two locators — `Gen.1` and `John.3`. */
+const workWithResolver = (resolver: string) => ({
+ systems: bookChapter,
+ works: {
+ 'test.work': `${workHeader()}
+citation_system: book-chapter
+resolvers:
+${resolver}
+references:
+ - 'Gen.1'
+ - 'John.3'
+`,
+ },
+});
+
+const urlFor = (reg: CompiledRegistry, locator: string) =>
+ reg.references.find((r) => r.locator === locator)?.resolver_targets[0]?.url;
+
+test('a url template expands the locator capture groups', () => {
+ const reg = compileFixture(
+ workWithResolver(` - url: 'https://example.org/{book}/{chapter}'`),
+ );
+ assert.equal(urlFor(reg, 'Gen.1'), 'https://example.org/Gen/1');
+ assert.equal(urlFor(reg, 'John.3'), 'https://example.org/John/3');
+ assert.equal(reg.warnings, 0);
+});
+
+test('url_by picks a whole URL by key, and an absent key skips the entry', () => {
+ const reg = compileFixture(
+ workWithResolver(` - url_by:
+ book:
+ Gen: 'https://example.org/genesis'`),
+ );
+ assert.equal(urlFor(reg, 'Gen.1'), 'https://example.org/genesis');
+ assert.equal(urlFor(reg, 'John.3'), undefined, 'John has no url_by entry');
+ assert.equal(reg.warnings, 1);
+});
+
+test('vars translates a locator value into the provider vocabulary', () => {
+ const reg = compileFixture(
+ workWithResolver(` - vars:
+ bookUsfm:
+ from: book
+ map:
+ Gen: GEN
+ John: JHN
+ url: 'https://example.org/{bookUsfm}.{chapter}/#{bookUsfm}.{chapter}'`),
+ );
+ assert.equal(urlFor(reg, 'Gen.1'), 'https://example.org/GEN.1/#GEN.1');
+ assert.equal(urlFor(reg, 'John.3'), 'https://example.org/JHN.3/#JHN.3');
+ assert.equal(reg.warnings, 0);
+});
+
+test('a hole in a vars map skips the entry and warns rather than emitting a wrong URL', () => {
+ const reg = compileFixture(
+ workWithResolver(` - vars:
+ bookUsfm:
+ from: book
+ map:
+ Gen: GEN
+ url: 'https://example.org/{bookUsfm}.{chapter}'`),
+ );
+ assert.equal(urlFor(reg, 'Gen.1'), 'https://example.org/GEN.1');
+ assert.equal(urlFor(reg, 'John.3'), undefined, 'John is unmapped');
+ assert.equal(reg.warnings, 1);
+});
+
+test('vars composes with url_by', () => {
+ const reg = compileFixture(
+ workWithResolver(` - vars:
+ slug:
+ from: book
+ map:
+ Gen: genesis
+ John: john
+ url_by:
+ slug:
+ genesis: 'https://example.org/a'
+ john: 'https://example.org/b'`),
+ );
+ assert.equal(urlFor(reg, 'Gen.1'), 'https://example.org/a');
+ assert.equal(urlFor(reg, 'John.3'), 'https://example.org/b');
+ assert.equal(reg.warnings, 0);
+});
+
+test('a vars name that shadows a locator variable is rejected', (t) => {
+ const message = expectCompileError(
+ t,
+ workWithResolver(` - vars:
+ book:
+ from: book
+ map:
+ Gen: GEN
+ url: 'https://example.org/{book}'`),
+ );
+ assert.match(message, /shadows a locator-derived variable/);
+});
+
+// Variable names and locator values are both author-controlled, so every lookup
+// on the way to a URL is own-property only. On a plain object these three cases
+// would inherit from Object.prototype and expand into a published URL.
+
+test('a template variable naming an Object.prototype member is absent, not inherited', () => {
+ const reg = compileFixture(
+ workWithResolver(` - url: 'https://example.org/{book}/{toString}'`),
+ );
+ assert.equal(urlFor(reg, 'Gen.1'), undefined);
+ assert.equal(urlFor(reg, 'John.3'), undefined);
+ assert.equal(reg.warnings, 2);
+});
+
+test('a locator value naming an Object.prototype member misses the vars map', () => {
+ const reg = compileFixture({
+ systems: bookChapter,
+ works: {
+ 'test.work': `${workHeader()}
+citation_system: book-chapter
+resolvers:
+ - vars:
+ bookUsfm:
+ from: book
+ map:
+ Gen: GEN
+ url: 'https://example.org/{bookUsfm}.{chapter}'
+references:
+ - 'Gen.1'
+ - 'toString.1'
+`,
+ },
+ });
+ assert.equal(urlFor(reg, 'Gen.1'), 'https://example.org/GEN.1');
+ assert.equal(
+ urlFor(reg, 'toString.1'),
+ undefined,
+ 'toString is not a mapped book',
+ );
+ assert.equal(reg.warnings, 1);
+});
+
+test('url_by with more than one selector variable is rejected at parse time', (t) => {
+ // The thrown message only names the file; the failing rule goes to
+ // console.error, so capture it rather than assert on any parse error.
+ const logged: string[] = [];
+ t.mock.method(console, 'error', (...args: unknown[]) => {
+ logged.push(args.join(' '));
+ });
+ assert.throws(() =>
+ compileFixture(
+ workWithResolver(` - url_by:
+ book:
+ Gen: 'https://example.org/genesis'
+ chapter:
+ '1': 'https://example.org/one'`),
+ ),
+ );
+ assert.match(logged.join('\n'), /url_by takes exactly one selector variable/);
+});
diff --git a/scripts/compile.ts b/scripts/compile.ts
index 884bd76..fa60b79 100644
--- a/scripts/compile.ts
+++ b/scripts/compile.ts
@@ -149,7 +149,11 @@ function deriveLocatorVars(
): Record {
const re = new RegExp(system.locator_regex);
const m = locator.match(re);
- const vars: Record = {};
+ // Null-prototype: variable names come from named capture groups and their
+ // values from locators, both author-controlled. On a plain object a name
+ // like `constructor` or `toString` would resolve up the prototype chain and
+ // expand into a URL instead of being treated as absent.
+ const vars: Record = Object.create(null);
if (m?.groups) {
for (const [k, v] of Object.entries(m.groups)) {
if (v === undefined) continue;
@@ -222,10 +226,42 @@ function expandTemplate(
return missing ? null : out;
}
-function buildResolverEntry(
+/**
+ * Bind a resolver's `vars` on top of the locator-derived variables. Each entry
+ * translates one canonical value into the provider's own spelling — OSIS `John`
+ * into die-bibel.de's USFM `JHN`. A source value with no entry in the map is
+ * treated exactly like a missing template variable: the caller skips the
+ * resolver for this reference and warns, so a hole in a book table shows up in
+ * the build output instead of silently emitting a wrong URL.
+ */
+function applyResolverVars(
resolver: ResolverEntry,
vars: Record,
+): Record | null {
+ if (!resolver.vars) return vars;
+ const out: Record = Object.assign(Object.create(null), vars);
+ for (const [name, spec] of Object.entries(resolver.vars)) {
+ if (Object.hasOwn(vars, name)) {
+ throw new Error(
+ `resolver var "${name}" shadows a locator-derived variable; give the mapped value its own name`,
+ );
+ }
+ const source = vars[spec.from];
+ if (source === undefined) return null;
+ // `map` is parsed YAML, so own-property only: a locator value of
+ // `toString` must miss the map, not inherit Object.prototype's method.
+ if (!Object.hasOwn(spec.map, source)) return null;
+ out[name] = spec.map[source];
+ }
+ return out;
+}
+
+function buildResolverEntry(
+ resolver: ResolverEntry,
+ locatorVars: Record,
): Record | null {
+ const vars = applyResolverVars(resolver, locatorVars);
+ if (!vars) return null;
let url: string | null = null;
if (resolver.url) {
url = expandTemplate(resolver.url, vars);
@@ -235,7 +271,8 @@ function buildResolverEntry(
const byVar = byVars[0];
const key = vars[byVar];
if (key === undefined) return null;
- url = resolver.url_by[byVar][key] ?? null;
+ const byMap = resolver.url_by[byVar];
+ url = Object.hasOwn(byMap, key) ? (byMap[key] ?? null) : null;
}
if (!url) return null;
const entry: Record = { url };
diff --git a/scripts/source-schema.ts b/scripts/source-schema.ts
index c5b01c3..5d2b0f7 100644
--- a/scripts/source-schema.ts
+++ b/scripts/source-schema.ts
@@ -11,6 +11,21 @@ export const ResolverEntrySource = z
.strictObject({
url: z.string().min(1).optional(),
url_by: z.record(z.string(), z.record(z.string(), z.string())).optional(),
+ // Provider-specific spellings of a locator variable. The canonical
+ // vocabulary stays in the locator (e.g. the OSIS book code `John`); a
+ // provider that names the same book differently (die-bibel.de's USFM
+ // `JHN`) declares the translation here rather than forcing a second
+ // citation system into existence. Compile-time only — nothing new
+ // reaches the published record, just a different expanded URL.
+ vars: z
+ .record(
+ z.string().min(1),
+ z.strictObject({
+ from: z.string().min(1),
+ map: z.record(z.string().min(1), z.string().min(1)),
+ }),
+ )
+ .optional(),
provider: z.string().min(1).optional(),
edition: z.string().min(1).optional(),
language: z.string().min(2).optional(),
@@ -21,6 +36,13 @@ export const ResolverEntrySource = z
})
.refine((r) => r.url !== undefined || r.url_by !== undefined, {
message: 'resolver needs either url or url_by',
+ })
+ // `url_by` selects a whole URL by the value of exactly one variable. More
+ // than one selector has no defined meaning, and the compiler would skip the
+ // entry per reference — an authoring typo would surface as silently missing
+ // resolver targets rather than a failed build.
+ .refine((r) => r.url_by === undefined || Object.keys(r.url_by).length === 1, {
+ message: 'url_by takes exactly one selector variable',
});
export type ResolverEntrySource = z.infer;
diff --git a/src/content/docs/get-started/authoring.md b/src/content/docs/get-started/authoring.md
index e55b7b8..b0c62b4 100644
--- a/src/content/docs/get-started/authoring.md
+++ b/src/content/docs/get-started/authoring.md
@@ -44,26 +44,21 @@ mappings:
modified: 2026-05-31
resolvers:
- - provider: Project Gutenberg
- edition: 'Müller (1881), Gutenberg ebook #2017'
+ - provider: SuttaCentral
+ edition: 'Bhikkhu Sujato translation'
language: en
access: open
- url: 'https://www.gutenberg.org/files/2017/2017-h/2017-h.htm#link2HCH{chapter04}'
+ url: 'https://suttacentral.net/dhp{verseGlobal}/en/sujato'
- - provider: Wikisource
+ - provider: ancient-buddhist-texts.net
language: en
access: open
url_by:
chapter:
- 1: 'https://en.wikisource.org/wiki/Dhammapada_(Muller)#Chapter_I:_The_Twin-Verses'
- 2: 'https://en.wikisource.org/wiki/Dhammapada_(Muller)#Chapter_II:_On_Earnestness'
+ 1: 'https://ancient-buddhist-texts.net/Texts-and-Translations/Dhammapada/01-Pairs.htm'
+ 2: 'https://ancient-buddhist-texts.net/Texts-and-Translations/Dhammapada/02-Heedfulness.htm'
# … one per chapter you have registered references for
- - provider: palikanon.com
- language: de
- access: open
- url: 'https://palikanon.com/khuddaka/dhp/dhp.html#dhp_{verse}'
-
references:
- '1.1'
- '1.2'
@@ -71,7 +66,9 @@ references:
# …
```
-Each reference gets one entry per resolver. The compiler expands `{chapter04}` and `{verse}` from the named capture groups in the citation system's `locator_regex`, and looks up `url_by.chapter[N]` for providers whose URL structure isn't templatable.
+Each reference gets one entry per resolver. The compiler derives `{verseGlobal}` from the citation system's `chapter_sizes` (see [How URL templates work](#how-url-templates-work)), and looks up `url_by.chapter[N]` for providers whose URL structure isn't templatable.
+
+Every resolver must actually address the cited passage. A URL with no locator variable in it resolves every reference in the work to the same landing page, which is worse than having no resolver at all — the registry says "here is 1.1" and hands the reader a book.
### Optional: `creators`
@@ -132,10 +129,36 @@ The compiler treats every resolver `url` as an [RFC 6570](https://www.rfc-editor
1. **Named capture groups** in the citation system's `locator_regex`. For example, a regex like `^(?\d+)\.(?\d+)$` exposes `{chapter}` and `{verse}` to every template.
2. **Zero-padded variants** of any numeric capture, generated automatically: `{chapter02}`, `{chapter03}`, `{chapter04}`, `{verse02}`, `{verse03}`. Use the padding width that matches the target site's URL.
3. **Roman-numeral variants** of any numeric capture in 1..3999, generated automatically: `{chapterRoman}` produces `I`, `VIII`, `XXVI`. Useful for sites that anchor sections by Roman chapter (e.g. Wikisource's `#I:8` Dhammapada verses).
-4. **Cumulative `{verseGlobal}`** — for systems whose locators have numeric `chapter` and `verse` groups _and_ declare `chapter_sizes:` (see below), the compiler exposes a global 1..N verse counter. Useful for single-page resolvers (e.g. palikanon.com's `#dhp_8`, `#dhp_102`) whose anchors use one running index across all chapters.
+4. **Cumulative `{verseGlobal}`** — for systems whose locators have numeric `chapter` and `verse` groups _and_ declare `chapter_sizes:` (see below), the compiler exposes a global 1..N verse counter. Useful for single-page resolvers (e.g. SuttaCentral's `/dhp8`, `/dhp102`) whose URLs use one running index across all chapters.
+5. **Provider-specific spellings** declared per resolver with `vars:` — see below.
If a template references a variable that doesn't exist for a given reference, the compiler skips that resolver entry for that reference and warns. Empty `resolver_targets` arrays are valid; references stay registered.
+## When a provider spells a locator value differently
+
+A locator carries one canonical vocabulary — for `bible-book-chapter-verse` that is the [OSIS book codes](https://wiki.crosswire.org/OSIS_Book_Abbreviations) (`Gen`, `John`, `1Cor`). Providers do not all agree with it: die-bibel.de addresses the same books with USFM codes (`GEN`, `JHN`, `1CO`). `vars:` declares that translation for one resolver, leaving the rest of the template intact:
+
+```yaml
+- provider: Deutsche Bibelgesellschaft
+ edition: 'Nestle-Aland, Novum Testamentum Graece, 28th edn (NA28)'
+ language: grc
+ access: open
+ vars:
+ bookUsfm:
+ from: book
+ map:
+ Matt: MAT
+ John: JHN
+ 1Cor: 1CO
+ url: 'https://www.die-bibel.de/bibel/NA28/{bookUsfm}.{chapter}/#{bookUsfm}.{chapter}.{verse}'
+```
+
+Each entry takes the value of `from` and looks it up in `map`, binding the result to a new variable usable anywhere in `url` or as the `url_by` key. The name must be new: a `vars` name that shadows a locator capture group is rejected, so `{book}` always means the canonical code no matter which resolver you are reading.
+
+A value with no entry in the map is treated exactly like a missing template variable — the entry is skipped and the compiler warns. A hole in a book table therefore shows up in the build output instead of quietly producing a wrong URL, so watch the skipped-entry count when filling one in.
+
+Reach for `vars:` when a provider renames _part_ of the URL; reach for `url_by:` (below) when the whole URL is arbitrary.
+
## When a URL pattern isn't templatable
Some providers use chapter or section names that don't fit a formula (e.g. `01-Pairs.htm`, `02-Heedfulness.htm`). For those, replace `url:` with a per-key map:
diff --git a/src/content/docs/get-started/index.md b/src/content/docs/get-started/index.md
index 6adce0e..9d382af 100644
--- a/src/content/docs/get-started/index.md
+++ b/src/content/docs/get-started/index.md
@@ -34,7 +34,7 @@ Four record types carry the model. `Work`, `CitationSystem`, `CanonicalReference
## Live examples
-- [`/id/work/dhammapada/`](/id/work/dhammapada/) — Dhammapada with four real providers (Gutenberg, Wikisource, ancient-buddhist-texts.net, palikanon.com) across English and German.
+- [`/id/work/dhammapada/`](/id/work/dhammapada/) — Dhammapada with three real providers (SuttaCentral in Pali and English, ancient-buddhist-texts.net).
- [`/id/work/plato.republic/`](/id/work/plato.republic/) — Plato's _Republic_ with Stephanus pagination.
- [`/cite/plato.republic/514a`](/cite/plato.republic/514a) — a short alias that redirects to the canonical reference URL. The qualified form, [`/cite/plato.republic/stephanus/514a`](/cite/plato.republic/stephanus/514a), resolves to the same page.
- [`/reg/`](/reg/) — the registry browser.
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 134b31d..93c9a5c 100644
--- a/src/content/docs/get-started/mappings-and-resolver-targets.md
+++ b/src/content/docs/get-started/mappings-and-resolver-targets.md
@@ -71,16 +71,17 @@ Each entry records what a reader needs to understand the link: `language` (BCP 4
"locator": "1.1",
"resolver_targets": [
{
- "url": "https://en.wikisource.org/wiki/Dhammapada_(Muller)#Chapter_I:_The_Twin-Verses",
- "language": "en",
- "edition": "Müller (1881)",
- "provider": "Wikisource",
+ "url": "https://suttacentral.net/dhp1/pli/ms",
+ "language": "pli",
+ "edition": "Mahāsaṅgīti Tipiṭaka",
+ "provider": "SuttaCentral",
"access": "open"
},
{
- "url": "https://palikanon.com/khuddaka/dhp/dhp.html#dhp_1",
- "language": "de",
- "provider": "palikanon.com",
+ "url": "https://ancient-buddhist-texts.net/Texts-and-Translations/Dhammapada/01-Pairs.htm",
+ "language": "en",
+ "edition": "Anandajoti Bhikkhu translation",
+ "provider": "ancient-buddhist-texts.net",
"access": "open"
}
]
From a95892890c5f2b30f9d1891be0916685a31985d8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
<14755525+maehr@users.noreply.github.com>
Date: Wed, 12 Aug 2026 22:38:26 +0200
Subject: [PATCH 34/45] chore(data): bump registry pin to the resolver review
---
data | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/data b/data
index 0f00bd4..237e39f 160000
--- a/data
+++ b/data
@@ -1 +1 @@
-Subproject commit 0f00bd4be5ed7039391ee3fce7c6e8c537d0a397
+Subproject commit 237e39f4767e9e65ffe4bee77c10f82d490cd223
From d2390c97608a9fa22f500aa9cb030048cd4a415b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
Date: Wed, 12 Aug 2026 22:53:24 +0200
Subject: [PATCH 35/45] chore(deps): refresh all dependencies before v0.1.0
(#73)
---
package-lock.json | 1858 ++++++++++++++++++++++-----------------------
1 file changed, 898 insertions(+), 960 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 19eac02..a431bef 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -69,16 +69,16 @@
}
},
"node_modules/@astrojs/check": {
- "version": "0.9.9",
- "resolved": "https://registry.npmjs.org/@astrojs/check/-/check-0.9.9.tgz",
- "integrity": "sha512-A5UW8uIuErLWEoRQvzgXpO1gTjUFtK8r7nU2Z7GewAMxUb7bPvpk11qaKKgxqXlHJWlAvaaxy+Xg28A6bmQ1Tg==",
+ "version": "0.9.10",
+ "resolved": "https://registry.npmjs.org/@astrojs/check/-/check-0.9.10.tgz",
+ "integrity": "sha512-zgx/UQMozdjOa3bOxjgeCFdtpE3c9rRX6xHwa+2QXvy8z8Akifu2AtubHyv/zzC2znO8dl8fFWL4K+Ba9kS8HQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@astrojs/language-server": "^2.16.7",
"chokidar": "^4.0.3",
"kleur": "^4.1.5",
- "yargs": "^17.7.2"
+ "yargs": "^18.0.0"
},
"bin": {
"astro-check": "bin/astro-check.js"
@@ -95,29 +95,29 @@
"license": "MIT"
},
"node_modules/@astrojs/compiler-binding": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding/-/compiler-binding-0.3.1.tgz",
- "integrity": "sha512-DaAUj29AIBU2XdJ8uwcab8lW5O2pk9pY8AXkcMw0sw77nVa3oeTYRcO+Dvbbpoexf6ThMc0FMWYCQ/wN1/T7oQ==",
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding/-/compiler-binding-0.3.2.tgz",
+ "integrity": "sha512-8w/9CWmYrAJJ8N0SY3O43ws2BgxoW6u3QsD8u2mE140lMYAlwh+tlNoUeSBq22wVheFuiBbR212l6ixZ2IIgCQ==",
"license": "MIT",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
- "@astrojs/compiler-binding-darwin-arm64": "0.3.1",
- "@astrojs/compiler-binding-darwin-x64": "0.3.1",
- "@astrojs/compiler-binding-linux-arm64-gnu": "0.3.1",
- "@astrojs/compiler-binding-linux-arm64-musl": "0.3.1",
- "@astrojs/compiler-binding-linux-x64-gnu": "0.3.1",
- "@astrojs/compiler-binding-linux-x64-musl": "0.3.1",
- "@astrojs/compiler-binding-wasm32-wasi": "0.3.1",
- "@astrojs/compiler-binding-win32-arm64-msvc": "0.3.1",
- "@astrojs/compiler-binding-win32-x64-msvc": "0.3.1"
+ "@astrojs/compiler-binding-darwin-arm64": "0.3.2",
+ "@astrojs/compiler-binding-darwin-x64": "0.3.2",
+ "@astrojs/compiler-binding-linux-arm64-gnu": "0.3.2",
+ "@astrojs/compiler-binding-linux-arm64-musl": "0.3.2",
+ "@astrojs/compiler-binding-linux-x64-gnu": "0.3.2",
+ "@astrojs/compiler-binding-linux-x64-musl": "0.3.2",
+ "@astrojs/compiler-binding-wasm32-wasi": "0.3.2",
+ "@astrojs/compiler-binding-win32-arm64-msvc": "0.3.2",
+ "@astrojs/compiler-binding-win32-x64-msvc": "0.3.2"
}
},
"node_modules/@astrojs/compiler-binding-darwin-arm64": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-arm64/-/compiler-binding-darwin-arm64-0.3.1.tgz",
- "integrity": "sha512-IEmEF2fUIlTHtpeE/isyEGVOB14cEyh/LZOFYt6wn3jNyVpdC8aR5OZ+RzFUR/f+8ZDM1LaMwZKvoA7eMyJeFw==",
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-arm64/-/compiler-binding-darwin-arm64-0.3.2.tgz",
+ "integrity": "sha512-MM8tn8CSimcfytaOla4b6acN8mKWiL/rlAA1fpT3/Wl7dNGSE4y8FjTN/zJVNnb63CsLWG5zZwCt01TXtDKh9g==",
"cpu": [
"arm64"
],
@@ -131,9 +131,9 @@
}
},
"node_modules/@astrojs/compiler-binding-darwin-x64": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-x64/-/compiler-binding-darwin-x64-0.3.1.tgz",
- "integrity": "sha512-GF2kIxjpPDLsn94zbZNMsxEmkU828QqnmM7kiQJnaooS3jmI+I7kk6+oI6EpwOsK3femCMdcm+wmOsEqtGrmjQ==",
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-x64/-/compiler-binding-darwin-x64-0.3.2.tgz",
+ "integrity": "sha512-2lXOlzf8xb7jLomRsf/aswh61/NnGusynB2OwFkK6k4pmOtpfXMYnG0PLfXrEvxXYj69NdCnmUYXtHDd+JOOag==",
"cpu": [
"x64"
],
@@ -147,9 +147,9 @@
}
},
"node_modules/@astrojs/compiler-binding-linux-arm64-gnu": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-gnu/-/compiler-binding-linux-arm64-gnu-0.3.1.tgz",
- "integrity": "sha512-XJL3SDmOtVrqFhCirNcHwE91+IesJqlgNo23I4qW9QUYfwzm/TBZuH61fgqsb1ttgR1mMYz6ooPWs0JDhwMqpQ==",
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-gnu/-/compiler-binding-linux-arm64-gnu-0.3.2.tgz",
+ "integrity": "sha512-BmU3kWj7qnLrd4vzm49zFEPJ5oFnn1tCT4Vt9hZbqdU5Cmb8GZl7fn6VFsnNfe7B18a2gIFtVzbLINtYl5kBjQ==",
"cpu": [
"arm64"
],
@@ -166,9 +166,9 @@
}
},
"node_modules/@astrojs/compiler-binding-linux-arm64-musl": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-musl/-/compiler-binding-linux-arm64-musl-0.3.1.tgz",
- "integrity": "sha512-xqE8BVbDoBueK/B47w30PtkVofUWJKGkwoMVE+EOMLf11rnoANxIAdA9FPqY+rng4oNI5ndHGsri1yPj2k8vZQ==",
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-musl/-/compiler-binding-linux-arm64-musl-0.3.2.tgz",
+ "integrity": "sha512-f0heT9ZZEseSu5bHCeb80eL2DH07ArE6U9xi1WT/PEusNjzPmEr3GJsjG1tRLo5VYUUYX7h3ScaqGmGrMOVGmw==",
"cpu": [
"arm64"
],
@@ -185,9 +185,9 @@
}
},
"node_modules/@astrojs/compiler-binding-linux-x64-gnu": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-gnu/-/compiler-binding-linux-x64-gnu-0.3.1.tgz",
- "integrity": "sha512-1y0StU1qiCuDFH3rmbRJXcxdfHxFPrES1Rd+RLffosvUR7I2cH5SF5SFnBN9vXpzpkmyElZm3Yr47iJBPN7vVA==",
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-gnu/-/compiler-binding-linux-x64-gnu-0.3.2.tgz",
+ "integrity": "sha512-M8fOUt0itRpqiGyoEA/ij184s8O+hqbCz3+YozRusOOM3osgGljpDThhbKAJjqh82wOo6FioQ4w8PBvU1XMD5Q==",
"cpu": [
"x64"
],
@@ -204,9 +204,9 @@
}
},
"node_modules/@astrojs/compiler-binding-linux-x64-musl": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-musl/-/compiler-binding-linux-x64-musl-0.3.1.tgz",
- "integrity": "sha512-16q0fYf7kpbmdObZEeZJEup8hQv/whgNwVjrSvT8umrKwLDSnNIWiQpm09lQQu6bweZB0XyIvHwlPitvJhC+hg==",
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-musl/-/compiler-binding-linux-x64-musl-0.3.2.tgz",
+ "integrity": "sha512-/Kebk8sO6HnLeSd691JkaAPfN7CqR9/KEXmWvyNPkaKNGmj8rTZ/lf2uXtnPu92Lan84UrKdIKVPy1fSo2encQ==",
"cpu": [
"x64"
],
@@ -223,25 +223,25 @@
}
},
"node_modules/@astrojs/compiler-binding-wasm32-wasi": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-wasm32-wasi/-/compiler-binding-wasm32-wasi-0.3.1.tgz",
- "integrity": "sha512-cB456shIwDv/PrVT+2QG7LFndpHkVge5HjqADKZgGaAc9JHVktCtjSrcdkRQ+3tbkPazNKaTLRjXLIiz2NIx9g==",
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-wasm32-wasi/-/compiler-binding-wasm32-wasi-0.3.2.tgz",
+ "integrity": "sha512-pUA6xbcOSB7DhfzIArB8BCAkFfAIqriiR7zl5zOStd6oU2G0kIKj+GUdGnyXyhfiv881Hffyk5tC0mR18sDjDw==",
"cpu": [
"wasm32"
],
"license": "MIT",
"optional": true,
"dependencies": {
- "@napi-rs/wasm-runtime": "^1.1.6"
+ "@napi-rs/wasm-runtime": "^1.2.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@astrojs/compiler-binding-win32-arm64-msvc": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-arm64-msvc/-/compiler-binding-win32-arm64-msvc-0.3.1.tgz",
- "integrity": "sha512-ur/9+If/yTE69mmeX5MqSZndL0HOyx67GeNZUy3N7wVdWpLz9UTJXwyWS4UR2PUQHitghjsM5xoX0Ge56WRVQQ==",
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-arm64-msvc/-/compiler-binding-win32-arm64-msvc-0.3.2.tgz",
+ "integrity": "sha512-ESruf+6Qkl1trHUFxI6GSf6t52j8yN2kCNSzMWdzt7V/T09tFHrYzrVaJQohb2C9bJUH76pNvX6Zb51+xCQc9Q==",
"cpu": [
"arm64"
],
@@ -255,9 +255,9 @@
}
},
"node_modules/@astrojs/compiler-binding-win32-x64-msvc": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-x64-msvc/-/compiler-binding-win32-x64-msvc-0.3.1.tgz",
- "integrity": "sha512-k0W+kDBzDkNZOqu4kElDvCOIbKw5Ut9S1WZ1Krj3KTgNuBERNKXsMMsRLLcbgfdMdbe7bTekQLshZrrvmYpmwA==",
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-x64-msvc/-/compiler-binding-win32-x64-msvc-0.3.2.tgz",
+ "integrity": "sha512-wzzVrEbOwbsLWOdEbocskjMRx2aZPxJ7ZbmL+jnpBamFwmigm+2M/wzuM6JWncocgYwLic1csSpalBh96kQKXA==",
"cpu": [
"x64"
],
@@ -271,26 +271,26 @@
}
},
"node_modules/@astrojs/compiler-rs": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/@astrojs/compiler-rs/-/compiler-rs-0.3.1.tgz",
- "integrity": "sha512-aT7xkgsbNoS6nriY5qKpbihK43slFHO41iqgHCTdOvn1ifaQxLCc5yXy+6GzAtiafoaC1zA7OwVXCXMsvUZOkg==",
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@astrojs/compiler-rs/-/compiler-rs-0.3.2.tgz",
+ "integrity": "sha512-xlx/T7JovIKduu4ucbTQUxQ5+Q8wxkHxhLjnZk3VlJbhbQ9RLvvuDk1p2YYFYFQ5y14dVm3FGGO4isQXa4F+Tg==",
"license": "MIT",
"dependencies": {
- "@astrojs/compiler-binding": "0.3.1"
+ "@astrojs/compiler-binding": "0.3.2"
},
"engines": {
"node": ">=22.12.0"
}
},
"node_modules/@astrojs/internal-helpers": {
- "version": "0.10.1",
- "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.10.1.tgz",
- "integrity": "sha512-5phcroT/vmOOrYuuAxtkbPixy5hePtlz9i8K4OeDv3dNK6/UQRuXPOSRTxIOBbUY5Sonw2UaxjbuVc43Mcir6Q==",
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.10.2.tgz",
+ "integrity": "sha512-yt7fMgPYqSM4Tmr+taTW6Per+hjJ8Pk6lA1PAcDyqzOt8HzJ6Kje5WzCxA2Sd+9wsUW7uhkLeoTMK0cXPwH9rQ==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.4",
"@types/mdast": "^4.0.4",
- "js-yaml": "^4.1.1",
+ "js-yaml": "^4.3.0",
"picomatch": "^4.0.4",
"retext-smartypants": "^6.2.0",
"shiki": "^4.0.2",
@@ -299,9 +299,9 @@
}
},
"node_modules/@astrojs/language-server": {
- "version": "2.16.11",
- "resolved": "https://registry.npmjs.org/@astrojs/language-server/-/language-server-2.16.11.tgz",
- "integrity": "sha512-sJ/EfnFp0+gurTrkvONtd9qRqmMZLT9bHelfI1SA35CaQVTrRrA74qteOcNT/al1b9Atg3IiH1Jk/qfckyC+fg==",
+ "version": "2.16.14",
+ "resolved": "https://registry.npmjs.org/@astrojs/language-server/-/language-server-2.16.14.tgz",
+ "integrity": "sha512-YPXkBu6N4d1sT09pvBmIDGZay+1MemV551FSgdEM3aZRDzbkxd2H7Cvf8MsVJLVB1mIEyTf1XbMN/30gp7s46w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -341,12 +341,12 @@
}
},
"node_modules/@astrojs/markdown-remark": {
- "version": "7.2.1",
- "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-7.2.1.tgz",
- "integrity": "sha512-jPVNIqTvk+yKviikszv/Y1U4jGUSKpp/Nw48QZV4qjWgp70j4Lkq3lhSDRbWwCfgKvEyO9GHuVbV1dM2WYXy1w==",
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-7.2.2.tgz",
+ "integrity": "sha512-FGfmK84zSNcrsBd0dl1gXE9JvZYElp8EXQa2jpHVAxG4deGKAp43wspxFupjADJX7MSsMRHwYCnfT6EyVmgeFQ==",
"license": "MIT",
"dependencies": {
- "@astrojs/internal-helpers": "0.10.1",
+ "@astrojs/internal-helpers": "0.10.2",
"@astrojs/prism": "4.0.2",
"github-slugger": "^2.0.0",
"hast-util-from-html": "^2.0.3",
@@ -366,12 +366,12 @@
}
},
"node_modules/@astrojs/markdown-satteri": {
- "version": "0.3.4",
- "resolved": "https://registry.npmjs.org/@astrojs/markdown-satteri/-/markdown-satteri-0.3.4.tgz",
- "integrity": "sha512-6Lvt/bQZEBW+zzdhPblvfZEy5PGEYJaUsUqaCgwHeRPxZJL1gc9I+DRLKWJjjYTWDzVUTzXlMq4WwSK+X34CVw==",
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/@astrojs/markdown-satteri/-/markdown-satteri-0.3.5.tgz",
+ "integrity": "sha512-CvWVEFAbay7YO+i9SaqDJubipA5ckiVB89QWoMJ5XC0m5CtFg8JwZ7Kau6X9sYY7FZURH0w2l03ISH2jOS/RDQ==",
"license": "MIT",
"dependencies": {
- "@astrojs/internal-helpers": "0.10.1",
+ "@astrojs/internal-helpers": "0.10.2",
"@astrojs/prism": "4.0.2",
"github-slugger": "^2.0.0",
"hast-util-from-html": "^2.0.3",
@@ -379,13 +379,13 @@
}
},
"node_modules/@astrojs/mdx": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-7.0.2.tgz",
- "integrity": "sha512-l+sJY5U1KkGZUdr+bIL4Y6BefeS549qoSHVSkUSs6A9INwdCND+/0+vN0NroPBXwl5Vcg5u78t7VQRsJjePxbw==",
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-7.0.5.tgz",
+ "integrity": "sha512-wEM/HH1RiEntyPVagdiF+yArzfcYLKBB0C1RZspVidKZ97rRMbaqP1Nbl/GR0sJs8zwaceqxRymw8aOKKJRdYw==",
"license": "MIT",
"dependencies": {
- "@astrojs/internal-helpers": "0.10.1",
- "@astrojs/markdown-remark": "7.2.1",
+ "@astrojs/internal-helpers": "0.10.2",
+ "@astrojs/markdown-remark": "7.2.2",
"@mdx-js/mdx": "^3.1.1",
"acorn": "^8.16.0",
"es-module-lexer": "^2.0.0",
@@ -447,14 +447,14 @@
}
},
"node_modules/@astrojs/starlight": {
- "version": "0.41.4",
- "resolved": "https://registry.npmjs.org/@astrojs/starlight/-/starlight-0.41.4.tgz",
- "integrity": "sha512-cRCKZhM2BKYViCakBiN68aVwPn5qj/XtMMq//G54xOWdXXcvic1gMMEI+veNlIKOqqC4QmIjcjk4jiFtlZ3mMg==",
+ "version": "0.41.7",
+ "resolved": "https://registry.npmjs.org/@astrojs/starlight/-/starlight-0.41.7.tgz",
+ "integrity": "sha512-579VJuZgo20UpNQPm9EIez5W3DFSrD16uiV2YX6rUlpLtjgKSdnc69TxVTZXn4AtI2B731TI2qhW1O3K+vwtrQ==",
"license": "MIT",
"dependencies": {
- "@astrojs/markdown-satteri": "^0.3.2",
- "@astrojs/mdx": "^7.0.0",
- "@astrojs/sitemap": "^3.7.2",
+ "@astrojs/markdown-satteri": "^0.3.5",
+ "@astrojs/mdx": "^7.0.5",
+ "@astrojs/sitemap": "^3.7.3",
"@pagefind/default-ui": "^1.3.0",
"@types/hast": "^3.0.4",
"@types/js-yaml": "^4.0.9",
@@ -551,12 +551,12 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
- "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
+ "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.29.7"
+ "@babel/types": "^7.29.8"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -576,9 +576,9 @@
}
},
"node_modules/@babel/types": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
- "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
+ "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.29.7",
@@ -595,96 +595,103 @@
"license": "MIT"
},
"node_modules/@bruits/satteri-darwin-arm64": {
- "version": "0.9.4",
- "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-arm64/-/satteri-darwin-arm64-0.9.4.tgz",
- "integrity": "sha512-W3MSUkr2mZRR8Stoe+lqNAyzQzRuFMU8WffV9IvFSxTok0LGWR0ZZQPLELU4QTRiUbhL2Y4VUP9vV7pj8rHjgg==",
+ "version": "0.9.5",
+ "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-arm64/-/satteri-darwin-arm64-0.9.5.tgz",
+ "integrity": "sha512-iw4nZgx9v30lWo/MTngQqi1pI78KI0DnkSm+lVJGYdmPLgAyDNJigVhpG42/Iq55A6c1Ll8q66ljyyRiQUxwow==",
"cpu": [
"arm64"
],
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@bruits/satteri-darwin-x64": {
- "version": "0.9.4",
- "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-x64/-/satteri-darwin-x64-0.9.4.tgz",
- "integrity": "sha512-DXOuuaE1lsv7mpk2mOvGrzqoEWEvOIZEO/fXVa7zfM23Iob+CBjBkRAMwpHA4pmZ3j6Gj7WJzPKw0kQ7w741AQ==",
+ "version": "0.9.5",
+ "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-x64/-/satteri-darwin-x64-0.9.5.tgz",
+ "integrity": "sha512-6T26Z5Kf3cFW2PSlk9p7zT7yVxvuBSiJvYyz9u8KjYwMTqZyIDOj2wDyNpxKV4+6yUVG7rddq2QwvG/8LJA2+Q==",
"cpu": [
"x64"
],
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@bruits/satteri-linux-arm64-gnu": {
- "version": "0.9.4",
- "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-gnu/-/satteri-linux-arm64-gnu-0.9.4.tgz",
- "integrity": "sha512-gJxU9rGGoqIznSEgEzpjxkry24jeHuMpoo1tCIAhHYh7WaD3j5F8zt3jmHxEaN1Uwa+K5+wFgIR2uIGOnMzEmw==",
+ "version": "0.9.5",
+ "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-gnu/-/satteri-linux-arm64-gnu-0.9.5.tgz",
+ "integrity": "sha512-u51id17uJwNEMK9nBlICsq6U31c+XVqQueVBkwRIzZG+gMpS8TOJctt5h5Wz33Z8xnMdTd+adtACVz0yHgGuOA==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@bruits/satteri-linux-arm64-musl": {
- "version": "0.9.4",
- "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-musl/-/satteri-linux-arm64-musl-0.9.4.tgz",
- "integrity": "sha512-Wjzu9hmmAbfmDkBfPI1VdZygJtYz9uYZQnkEyrXi6S2JFi+2pXQ1A5irj38bqm0IZmWcTbk0cVG4NZnPdtVNJA==",
+ "version": "0.9.5",
+ "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-musl/-/satteri-linux-arm64-musl-0.9.5.tgz",
+ "integrity": "sha512-v39HxiwGC5Rqm01HksP6+5Y+xKLPlsuVFgIgpEAo+SiQ22c+mJVhS3u7Z6ePAKdhL5NJoK1xq70kLz3L13AhpQ==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@bruits/satteri-linux-x64-gnu": {
- "version": "0.9.4",
- "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-gnu/-/satteri-linux-x64-gnu-0.9.4.tgz",
- "integrity": "sha512-MR1Q+wMx65FQlbSV7cRqWW87Knp0zkoaIV55Dt+xZl028wJABXEPEEmG3670SLq7lVZvcGIDwCgSg2kCYxvRwA==",
+ "version": "0.9.5",
+ "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-gnu/-/satteri-linux-x64-gnu-0.9.5.tgz",
+ "integrity": "sha512-F3uO8uFp3pAP5ZGXttwvh57GS7s0lL953tnNdyI2gRyP4kOOkp6pyGojNJzCjkDvWI2Cvb9iNrKok3aqQPauAw==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@bruits/satteri-linux-x64-musl": {
- "version": "0.9.4",
- "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-musl/-/satteri-linux-x64-musl-0.9.4.tgz",
- "integrity": "sha512-T4gxhXve3zyNAZesrXAd/rDZOGRkbfFIUFld4TGsw6BsjoIteCcDji6IMqeXyaWEVSykY2X8Eid2hr6aXGYAaw==",
+ "version": "0.9.5",
+ "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-musl/-/satteri-linux-x64-musl-0.9.5.tgz",
+ "integrity": "sha512-bicEqglLlz++mWyADaZoP0JY20s4vDfLjaPYgQqC+NI4zZLTOOg1T4GB8aqtc822Pqji8SQBmSrTb7CrP8i08Q==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@bruits/satteri-wasm32-wasi": {
- "version": "0.9.4",
- "resolved": "https://registry.npmjs.org/@bruits/satteri-wasm32-wasi/-/satteri-wasm32-wasi-0.9.4.tgz",
- "integrity": "sha512-/CEG8LUlpaBEnhFnYVn0UnlHFLs51UhrkJBUPDUXLzkadzAcnR88iRA/nOl7Zwhjb4WhfBV4p3P5qeOJMtH0iA==",
+ "version": "0.9.5",
+ "resolved": "https://registry.npmjs.org/@bruits/satteri-wasm32-wasi/-/satteri-wasm32-wasi-0.9.5.tgz",
+ "integrity": "sha512-zauAuMwfPnKPUkd4AFixRFpXdgKwP2mKgxrIIo2gJzW0/ZneF9dbHnLkojSpaBnCCp7VUL1hIi5WWZvB1CqmAQ==",
"cpu": [
"wasm32"
],
+ "license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "1.11.1",
@@ -716,25 +723,37 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@bruits/satteri-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
+ "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@bruits/satteri-win32-arm64-msvc": {
- "version": "0.9.4",
- "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-arm64-msvc/-/satteri-win32-arm64-msvc-0.9.4.tgz",
- "integrity": "sha512-E1ZPQbgCtFKiU7pFYVndynvY7ne4coeVDUgnVThErSFlJ2ceQCBZrfRTD1lzrIDy63Bbqo+g/cZY9duw+JYjIw==",
+ "version": "0.9.5",
+ "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-arm64-msvc/-/satteri-win32-arm64-msvc-0.9.5.tgz",
+ "integrity": "sha512-SrfE7NEsgZjBvU3c+RR6oQRu0ToXY5uVJEbieXEF0YTctIV2zAVlbaMjWLts074QCgh3a+XHWkR/lWh2VH2LUg==",
"cpu": [
"arm64"
],
+ "license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@bruits/satteri-win32-x64-msvc": {
- "version": "0.9.4",
- "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-x64-msvc/-/satteri-win32-x64-msvc-0.9.4.tgz",
- "integrity": "sha512-5I7SiarsNdAUuhJb50CXJPTwr/ECVrBoU+fymoLjChK5fW//+srhY4lstcNTzgFRtQSYfVtm4OQZz16CVMeTeA==",
+ "version": "0.9.5",
+ "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-x64-msvc/-/satteri-win32-x64-msvc-0.9.5.tgz",
+ "integrity": "sha512-5Kw9ZAtTGS8WHizyn+CJhjjfIQrw+7jcZodpmpXJjefnO15M8UexIi6JR2E5thyvsmHyhL6ZDDMUNR4bKJPd4g==",
"cpu": [
"x64"
],
+ "license": "MIT",
"optional": true,
"os": [
"win32"
@@ -809,75 +828,6 @@
"node": ">=22.12.0"
}
},
- "node_modules/@commitlint/cli/node_modules/cliui": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
- "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "string-width": "^7.2.0",
- "strip-ansi": "^7.1.0",
- "wrap-ansi": "^9.0.0"
- },
- "engines": {
- "node": ">=20"
- }
- },
- "node_modules/@commitlint/cli/node_modules/string-width": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
- "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^10.3.0",
- "get-east-asian-width": "^1.0.0",
- "strip-ansi": "^7.1.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@commitlint/cli/node_modules/wrap-ansi": {
- "version": "9.0.2",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
- "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^6.2.1",
- "string-width": "^7.0.0",
- "strip-ansi": "^7.1.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/@commitlint/cli/node_modules/yargs": {
- "version": "18.0.0",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
- "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cliui": "^9.0.1",
- "escalade": "^3.1.1",
- "get-caller-file": "^2.0.5",
- "string-width": "^7.2.0",
- "y18n": "^5.0.5",
- "yargs-parser": "^22.0.0"
- },
- "engines": {
- "node": "^20.19.0 || ^22.12.0 || >=23"
- }
- },
"node_modules/@commitlint/config-conventional": {
"version": "21.2.0",
"resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-21.2.0.tgz",
@@ -1107,9 +1057,9 @@
}
},
"node_modules/@conventional-changelog/git-client": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/@conventional-changelog/git-client/-/git-client-3.1.0.tgz",
- "integrity": "sha512-Tqa/gHco2WJWa740NRjOrfKVvzIqxkZpecb8bemaQ8sKM5PXb1UK4uTyTb/1wIqNuOVaDOFxyBdhTIQZn6gdjQ==",
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@conventional-changelog/git-client/-/git-client-3.1.2.tgz",
+ "integrity": "sha512-jZqwnJwf7nboIlAcw/mkOjVa6DexCcUOgT2oOQgkoi3z9vR8tGFkcMy2BFcYwjhL9sYcDDXkRQDayiDieCoW7A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1122,7 +1072,7 @@
},
"peerDependencies": {
"conventional-commits-filter": "^6.0.1",
- "conventional-commits-parser": "^7.0.1"
+ "conventional-commits-parser": "^7.1.2"
},
"peerDependenciesMeta": {
"conventional-commits-filter": {
@@ -1134,9 +1084,9 @@
}
},
"node_modules/@conventional-changelog/template": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@conventional-changelog/template/-/template-1.2.1.tgz",
- "integrity": "sha512-TzlTVpKPjaqW6qOYjQcYUDuGsLCNsvFHVBXkYGTAnf5V37jCWrE5haKNXzz0WZUtVHjrpV76L1buANjwXMfT8w==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@conventional-changelog/template/-/template-1.3.0.tgz",
+ "integrity": "sha512-GsCw/qu92GI0EX6s7fxUi/SG1lmFjG9XZivxxEDZXqztuQKCn5o5wKdz4v005zci0Md7EZzgAwmQLoIEDZoaow==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1215,21 +1165,21 @@
"license": "MIT"
},
"node_modules/@emnapi/core": {
- "version": "1.11.2",
- "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz",
- "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==",
+ "version": "1.11.3",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz",
+ "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
- "@emnapi/wasi-threads": "1.2.2",
+ "@emnapi/wasi-threads": "1.2.3",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
- "version": "1.11.2",
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz",
- "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==",
+ "version": "1.11.3",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
+ "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
"license": "MIT",
"optional": true,
"dependencies": {
@@ -1237,19 +1187,20 @@
}
},
"node_modules/@emnapi/wasi-threads": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
- "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz",
+ "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==",
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
- "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
+ "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
"cpu": [
"ppc64"
],
@@ -1263,9 +1214,9 @@
}
},
"node_modules/@esbuild/android-arm": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
- "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
+ "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
"cpu": [
"arm"
],
@@ -1279,9 +1230,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
- "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
+ "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
"cpu": [
"arm64"
],
@@ -1295,9 +1246,9 @@
}
},
"node_modules/@esbuild/android-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
- "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
+ "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
"cpu": [
"x64"
],
@@ -1311,9 +1262,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
- "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
+ "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
"cpu": [
"arm64"
],
@@ -1327,9 +1278,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
- "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
+ "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
"cpu": [
"x64"
],
@@ -1343,9 +1294,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
- "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
"cpu": [
"arm64"
],
@@ -1359,9 +1310,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
- "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
+ "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
"cpu": [
"x64"
],
@@ -1375,9 +1326,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
- "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
+ "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
"cpu": [
"arm"
],
@@ -1391,9 +1342,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
- "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
+ "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
"cpu": [
"arm64"
],
@@ -1407,9 +1358,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
- "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
+ "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
"cpu": [
"ia32"
],
@@ -1423,9 +1374,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
- "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
+ "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
"cpu": [
"loong64"
],
@@ -1439,9 +1390,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
- "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
+ "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
"cpu": [
"mips64el"
],
@@ -1455,9 +1406,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
- "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
+ "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
"cpu": [
"ppc64"
],
@@ -1471,9 +1422,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
- "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
+ "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
"cpu": [
"riscv64"
],
@@ -1487,9 +1438,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
- "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
+ "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
"cpu": [
"s390x"
],
@@ -1503,9 +1454,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
- "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
+ "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
"cpu": [
"x64"
],
@@ -1519,9 +1470,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
- "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
"cpu": [
"arm64"
],
@@ -1535,9 +1486,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
- "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
+ "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
"cpu": [
"x64"
],
@@ -1551,9 +1502,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
- "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
"cpu": [
"arm64"
],
@@ -1567,9 +1518,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
- "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
+ "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
"cpu": [
"x64"
],
@@ -1583,9 +1534,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
- "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
+ "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
"cpu": [
"arm64"
],
@@ -1599,9 +1550,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
- "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
+ "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
"cpu": [
"x64"
],
@@ -1615,9 +1566,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
- "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
+ "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
"cpu": [
"arm64"
],
@@ -1631,9 +1582,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
- "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
+ "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
"cpu": [
"ia32"
],
@@ -1647,9 +1598,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
- "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
+ "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
"cpu": [
"x64"
],
@@ -1663,9 +1614,9 @@
}
},
"node_modules/@expressive-code/core": {
- "version": "0.44.0",
- "resolved": "https://registry.npmjs.org/@expressive-code/core/-/core-0.44.0.tgz",
- "integrity": "sha512-xgiF2P6tYUbrhi3+x0S8xHZWT1t3Bvb3U91tAtRbLb9HLejLvYc5GZUqKICKLaUN4iSGhhNJu2fM/aH8e5yCMg==",
+ "version": "0.44.1",
+ "resolved": "https://registry.npmjs.org/@expressive-code/core/-/core-0.44.1.tgz",
+ "integrity": "sha512-3dDo9N8D7hYrLNNMMWFovg3+aDUtnQm7c7z0GZc1c0LEFVBc0Q6lKG+tVT28gDadOvsgOANfCn35fgpe97Pmgg==",
"license": "MIT",
"dependencies": {
"@ctrl/tinycolor": "^4.0.4",
@@ -1680,31 +1631,31 @@
}
},
"node_modules/@expressive-code/plugin-frames": {
- "version": "0.44.0",
- "resolved": "https://registry.npmjs.org/@expressive-code/plugin-frames/-/plugin-frames-0.44.0.tgz",
- "integrity": "sha512-V6M6+zVc1GzqCvXkQHc2m5rcFOIVzJgMq5gnfrMnVf2gwtj/sg4H93c1f/mGeqHycubwkHFUDyParAOiGeDZeA==",
+ "version": "0.44.1",
+ "resolved": "https://registry.npmjs.org/@expressive-code/plugin-frames/-/plugin-frames-0.44.1.tgz",
+ "integrity": "sha512-HC/bdRao9225ApcgO/e3jn8ZOhldKO7ob1O/Tcipvtv7Vb5nMphZhMtD9uuywpvxkPYBHJi3504WhrKg05Dwqg==",
"license": "MIT",
"dependencies": {
- "@expressive-code/core": "^0.44.0"
+ "@expressive-code/core": "^0.44.1"
}
},
"node_modules/@expressive-code/plugin-shiki": {
- "version": "0.44.0",
- "resolved": "https://registry.npmjs.org/@expressive-code/plugin-shiki/-/plugin-shiki-0.44.0.tgz",
- "integrity": "sha512-RZsdaqlbGqyAQKuoX4myQXxjmiE2l5KBpJ/gKPh62tCdIdpWyjbzVqSo8+5XsezZxkfi8AJ/J6EUaBTPROFX/Q==",
+ "version": "0.44.1",
+ "resolved": "https://registry.npmjs.org/@expressive-code/plugin-shiki/-/plugin-shiki-0.44.1.tgz",
+ "integrity": "sha512-YApiZt3buUzBwL5tqj8G+sYC5NjMjRCHgQwr9bmGl69rtcHy6fE9dooWUeKYB978fJT2BuxT5FeHcF47rA3SEg==",
"license": "MIT",
"dependencies": {
- "@expressive-code/core": "^0.44.0",
+ "@expressive-code/core": "^0.44.1",
"shiki": "^4.0.2"
}
},
"node_modules/@expressive-code/plugin-text-markers": {
- "version": "0.44.0",
- "resolved": "https://registry.npmjs.org/@expressive-code/plugin-text-markers/-/plugin-text-markers-0.44.0.tgz",
- "integrity": "sha512-0/m3A5b+lz2upyNq+wzZ1S69HRoJmyFs5LsR42lVZ9pmGRlBiSBYQpvqlji4DBj1+Riamxc0AvcCr5kuzOQeWA==",
+ "version": "0.44.1",
+ "resolved": "https://registry.npmjs.org/@expressive-code/plugin-text-markers/-/plugin-text-markers-0.44.1.tgz",
+ "integrity": "sha512-B3BsJoJ8CFMlcIX9f+X9tcI3C4zPDO601+YuLi9GheSTNro7ZfqSjLptMQKBHOWZvxnAtY5zvIX7iO/qtBhNBg==",
"license": "MIT",
"dependencies": {
- "@expressive-code/core": "^0.44.0"
+ "@expressive-code/core": "^0.44.1"
}
},
"node_modules/@humanwhocodes/momoa": {
@@ -2335,27 +2286,30 @@
}
},
"node_modules/@napi-rs/wasm-runtime": {
- "version": "1.1.6",
- "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
- "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz",
+ "integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==",
"license": "MIT",
"optional": true,
"dependencies": {
"@tybys/wasm-util": "^0.10.3"
},
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=23.5.0"
+ },
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
},
"peerDependencies": {
- "@emnapi/core": "^1.7.1",
- "@emnapi/runtime": "^1.7.1"
+ "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4",
+ "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4"
}
},
"node_modules/@nodable/entities": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.2.0.tgz",
- "integrity": "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.0.0.tgz",
+ "integrity": "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==",
"funding": [
{
"type": "github",
@@ -2371,9 +2325,9 @@
"license": "MIT"
},
"node_modules/@oxc-project/types": {
- "version": "0.138.0",
- "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz",
- "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==",
+ "version": "0.144.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz",
+ "integrity": "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/Boshen"
@@ -2529,9 +2483,9 @@
}
},
"node_modules/@rolldown/binding-android-arm64": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz",
- "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz",
+ "integrity": "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==",
"cpu": [
"arm64"
],
@@ -2545,9 +2499,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz",
- "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.4.tgz",
+ "integrity": "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==",
"cpu": [
"arm64"
],
@@ -2561,9 +2515,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz",
- "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.4.tgz",
+ "integrity": "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==",
"cpu": [
"x64"
],
@@ -2577,9 +2531,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz",
- "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.4.tgz",
+ "integrity": "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==",
"cpu": [
"x64"
],
@@ -2593,9 +2547,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz",
- "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.4.tgz",
+ "integrity": "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==",
"cpu": [
"arm"
],
@@ -2609,9 +2563,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz",
- "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.4.tgz",
+ "integrity": "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==",
"cpu": [
"arm64"
],
@@ -2628,9 +2582,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz",
- "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.4.tgz",
+ "integrity": "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==",
"cpu": [
"arm64"
],
@@ -2647,9 +2601,9 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz",
- "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.4.tgz",
+ "integrity": "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==",
"cpu": [
"ppc64"
],
@@ -2666,9 +2620,9 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz",
- "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.4.tgz",
+ "integrity": "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==",
"cpu": [
"s390x"
],
@@ -2685,9 +2639,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz",
- "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.4.tgz",
+ "integrity": "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==",
"cpu": [
"x64"
],
@@ -2704,9 +2658,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz",
- "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.4.tgz",
+ "integrity": "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==",
"cpu": [
"x64"
],
@@ -2723,9 +2677,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz",
- "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.4.tgz",
+ "integrity": "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==",
"cpu": [
"arm64"
],
@@ -2738,49 +2692,10 @@
"node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@rolldown/binding-wasm32-wasi": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz",
- "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==",
- "cpu": [
- "wasm32"
- ],
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/core": "1.11.1",
- "@emnapi/runtime": "1.11.1",
- "@napi-rs/wasm-runtime": "^1.1.6"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
- "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/wasi-threads": "1.2.2",
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
- "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
"node_modules/@rolldown/binding-win32-arm64-msvc": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz",
- "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.4.tgz",
+ "integrity": "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==",
"cpu": [
"arm64"
],
@@ -2794,9 +2709,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz",
- "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.4.tgz",
+ "integrity": "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==",
"cpu": [
"x64"
],
@@ -2815,34 +2730,6 @@
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"license": "MIT"
},
- "node_modules/@rollup/pluginutils": {
- "version": "5.4.0",
- "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz",
- "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==",
- "license": "MIT",
- "dependencies": {
- "@types/estree": "^1.0.0",
- "estree-walker": "^2.0.2",
- "picomatch": "^4.0.2"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "peerDependencies": {
- "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
- },
- "peerDependenciesMeta": {
- "rollup": {
- "optional": true
- }
- }
- },
- "node_modules/@rollup/pluginutils/node_modules/estree-walker": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
- "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
- "license": "MIT"
- },
"node_modules/@sec-ant/readable-stream": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
@@ -2851,15 +2738,15 @@
"license": "MIT"
},
"node_modules/@shikijs/core": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.3.1.tgz",
- "integrity": "sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==",
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.4.3.tgz",
+ "integrity": "sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==",
"license": "MIT",
"dependencies": {
- "@shikijs/primitive": "4.3.1",
- "@shikijs/types": "4.3.1",
+ "@shikijs/primitive": "4.4.3",
+ "@shikijs/types": "4.4.3",
"@shikijs/vscode-textmate": "^10.0.2",
- "@types/hast": "^3.0.4",
+ "@types/hast": "^3.0.5",
"hast-util-to-html": "^9.0.5"
},
"engines": {
@@ -2867,12 +2754,12 @@
}
},
"node_modules/@shikijs/engine-javascript": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.3.1.tgz",
- "integrity": "sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ==",
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.4.3.tgz",
+ "integrity": "sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ==",
"license": "MIT",
"dependencies": {
- "@shikijs/types": "4.3.1",
+ "@shikijs/types": "4.4.3",
"@shikijs/vscode-textmate": "^10.0.2",
"oniguruma-to-es": "^4.3.6"
},
@@ -2881,12 +2768,12 @@
}
},
"node_modules/@shikijs/engine-oniguruma": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.3.1.tgz",
- "integrity": "sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg==",
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.4.3.tgz",
+ "integrity": "sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w==",
"license": "MIT",
"dependencies": {
- "@shikijs/types": "4.3.1",
+ "@shikijs/types": "4.4.3",
"@shikijs/vscode-textmate": "^10.0.2"
},
"engines": {
@@ -2894,51 +2781,51 @@
}
},
"node_modules/@shikijs/langs": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.3.1.tgz",
- "integrity": "sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ==",
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.4.3.tgz",
+ "integrity": "sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A==",
"license": "MIT",
"dependencies": {
- "@shikijs/types": "4.3.1"
+ "@shikijs/types": "4.4.3"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@shikijs/primitive": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.3.1.tgz",
- "integrity": "sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A==",
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.4.3.tgz",
+ "integrity": "sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ==",
"license": "MIT",
"dependencies": {
- "@shikijs/types": "4.3.1",
+ "@shikijs/types": "4.4.3",
"@shikijs/vscode-textmate": "^10.0.2",
- "@types/hast": "^3.0.4"
+ "@types/hast": "^3.0.5"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@shikijs/themes": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.3.1.tgz",
- "integrity": "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA==",
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.4.3.tgz",
+ "integrity": "sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw==",
"license": "MIT",
"dependencies": {
- "@shikijs/types": "4.3.1"
+ "@shikijs/types": "4.4.3"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@shikijs/types": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.3.1.tgz",
- "integrity": "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g==",
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.4.3.tgz",
+ "integrity": "sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g==",
"license": "MIT",
"dependencies": {
"@shikijs/vscode-textmate": "^10.0.2",
- "@types/hast": "^3.0.4"
+ "@types/hast": "^3.0.5"
},
"engines": {
"node": ">=20"
@@ -3148,9 +3035,9 @@
"license": "MIT"
},
"node_modules/@types/d3-geo": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
- "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.1.tgz",
+ "integrity": "sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==",
"license": "MIT",
"dependencies": {
"@types/geojson": "*"
@@ -3293,9 +3180,9 @@
"license": "MIT"
},
"node_modules/@types/hast": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
- "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz",
+ "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==",
"license": "MIT",
"dependencies": {
"@types/unist": "*"
@@ -3355,9 +3242,9 @@
}
},
"node_modules/@types/node": {
- "version": "26.1.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz",
- "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==",
+ "version": "26.2.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz",
+ "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==",
"license": "MIT",
"dependencies": {
"undici-types": "~8.3.0"
@@ -3393,9 +3280,9 @@
"license": "MIT"
},
"node_modules/@ungap/structured-clone": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.2.tgz",
- "integrity": "sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==",
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz",
+ "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==",
"license": "ISC"
},
"node_modules/@upsetjs/venn.js": {
@@ -3514,9 +3401,9 @@
"license": "MIT"
},
"node_modules/acorn": {
- "version": "8.17.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
- "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
+ "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@@ -3605,9 +3492,9 @@
}
},
"node_modules/ansi-regex": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
- "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz",
+ "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3618,13 +3505,16 @@
}
},
"node_modules/ansi-styles": {
- "version": "6.2.3",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
- "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
"engines": {
- "node": ">=12"
+ "node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
@@ -3679,6 +3569,19 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"license": "Python-2.0"
},
+ "node_modules/argue-cli": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/argue-cli/-/argue-cli-3.1.0.tgz",
+ "integrity": "sha512-DhBpBfXL4SS2uC0N922MMajKR3CdrTG0u2or1PNYgXMsrSzViJrbtvT0nCLlLGUI0plam/ZZCs7aAauHtW9thw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=22"
+ },
+ "funding": {
+ "url": "https://ko-fi.com/dangreen"
+ }
+ },
"node_modules/aria-query": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
@@ -3708,19 +3611,18 @@
}
},
"node_modules/astro": {
- "version": "7.1.3",
- "resolved": "https://registry.npmjs.org/astro/-/astro-7.1.3.tgz",
- "integrity": "sha512-4dhPyAAXthf3xLEYnG8SeL7yr/nTPPABfY7e9YF0yuO+vK9Xp+8Q5j4xzsmL3GueukQv4oNwGNTBepLOiDGeJA==",
+ "version": "7.2.1",
+ "resolved": "https://registry.npmjs.org/astro/-/astro-7.2.1.tgz",
+ "integrity": "sha512-ynyMTiyF//GLcd8+gRNbvcKgS1ht+qTgp/fkkHIpGYNILLseQqQRuPZ9PqXYKfsact4J1emZaXRDpg2bl35Pag==",
"license": "MIT",
"dependencies": {
- "@astrojs/compiler-rs": "^0.3.1",
- "@astrojs/internal-helpers": "0.10.1",
- "@astrojs/markdown-satteri": "0.3.4",
+ "@astrojs/compiler-rs": "^0.3.2",
+ "@astrojs/internal-helpers": "0.10.2",
+ "@astrojs/markdown-satteri": "0.3.5",
"@astrojs/telemetry": "3.3.3",
"@capsizecss/unpack": "^4.0.0",
"@clack/prompts": "^1.1.0",
"@oslojs/encoding": "^1.1.0",
- "@rollup/pluginutils": "^5.3.0",
"am-i-vibing": "^0.4.0",
"aria-query": "^5.3.2",
"axobject-query": "^4.1.0",
@@ -3739,9 +3641,9 @@
"github-slugger": "^2.0.0",
"html-escaper": "3.0.3",
"http-cache-semantics": "^4.2.0",
- "js-yaml": "^4.1.1",
+ "js-yaml": "^4.3.0",
"jsonc-parser": "^3.3.1",
- "magic-string": "^0.30.21",
+ "magic-string": "^1.0.0",
"magicast": "^0.5.2",
"mrmime": "^2.0.1",
"neotraverse": "^1.0.1",
@@ -3783,7 +3685,7 @@
"sharp": "^0.34.0 || ^0.35.0"
},
"peerDependencies": {
- "@astrojs/markdown-remark": "7.2.1"
+ "@astrojs/markdown-remark": "7.2.2"
},
"peerDependenciesMeta": {
"@astrojs/markdown-remark": {
@@ -3792,12 +3694,12 @@
}
},
"node_modules/astro-expressive-code": {
- "version": "0.44.0",
- "resolved": "https://registry.npmjs.org/astro-expressive-code/-/astro-expressive-code-0.44.0.tgz",
- "integrity": "sha512-b1wN/ZvbJprzxlGKIpIes2kQrCY5KRLwys2tWbZAZyjGZcW5ZtgneZnBwzNRiBna9/48d4mQl19KLjcRuhO1hw==",
+ "version": "0.44.1",
+ "resolved": "https://registry.npmjs.org/astro-expressive-code/-/astro-expressive-code-0.44.1.tgz",
+ "integrity": "sha512-DT1LnCqbHasBKlvzJ3m6LR4VI94wwx3W9EV/YbP1te4rqjOHsvsezHYuqb5MeLWLftXms/1FA9QBbwCo43DnJQ==",
"license": "MIT",
"dependencies": {
- "rehype-expressive-code": "^0.44.0",
+ "rehype-expressive-code": "^0.44.1",
"url-extras": "^0.1.0"
},
"peerDependencies": {
@@ -3825,6 +3727,15 @@
}
}
},
+ "node_modules/astro/node_modules/magic-string": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.1.1.tgz",
+ "integrity": "sha512-qFemKPzc3ttrYVaMmnSkGtGc5nE6Ncl4bj7c9IE6C9OUIRXjf6PzJ+UZ1xhVIjYc7dolHq3qKzpAJPZUbvNj+A==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -3946,22 +3857,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
- "node_modules/chalk/node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
"node_modules/character-entities": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
@@ -4034,107 +3929,36 @@
}
},
"node_modules/cliui": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
- "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
+ "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
"dev": true,
"license": "ISC",
"dependencies": {
- "string-width": "^4.2.0",
- "strip-ansi": "^6.0.1",
- "wrap-ansi": "^7.0.0"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/cliui/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/cliui/node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
+ "string-width": "^7.2.0",
+ "strip-ansi": "^7.1.0",
+ "wrap-ansi": "^9.0.0"
},
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/cliui/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/cliui/node_modules/is-fullwidth-code-point": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
- "dev": true,
- "license": "MIT",
"engines": {
- "node": ">=8"
+ "node": ">=20"
}
},
"node_modules/cliui/node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/cliui/node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/cliui/node_modules/wrap-ansi": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
- "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
},
"engines": {
- "node": ">=10"
+ "node": ">=18"
},
"funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/clsx": {
@@ -4218,40 +4042,40 @@
}
},
"node_modules/conventional-changelog-angular": {
- "version": "9.2.1",
- "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-9.2.1.tgz",
- "integrity": "sha512-oWSL6ZhnXbYraOFTK3PgRAQJ8fADDAEv5K6AdeyQPLvjFmhG8+ejL0jZZp/R7vTmGJaBvZEE+sE7dB4bCv7sAw==",
+ "version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-9.3.0.tgz",
+ "integrity": "sha512-0MWQLVUT1oVCsUGs9aAWteBVxPlLwJTn5VbQH7B0B3fDizZgrJ9QGnKl/2mp1+5P7153GCBCjO/v1aKJ6eysCg==",
"dev": true,
"license": "ISC",
"dependencies": {
- "@conventional-changelog/template": "^1.2.1"
+ "@conventional-changelog/template": "^1.3.0"
},
"engines": {
"node": ">=22"
}
},
"node_modules/conventional-changelog-conventionalcommits": {
- "version": "10.2.1",
- "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-10.2.1.tgz",
- "integrity": "sha512-n4Kr1HFMTf3iMbES0TMxKIcYtUUv4rKqyQQp2JwfOEfFCOfGT3Tq4mCyJ8S9/YPyWhydjfKrrvnyl+gCjA+mJQ==",
+ "version": "10.3.0",
+ "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-10.3.0.tgz",
+ "integrity": "sha512-qag0zFD867Qq1DK0jAWicyWlEMS1FFC/BLVLISaoeI7Y6Em6aWchk7BCkhOTsecRpMsV6qX41XmlNnghiiTmSw==",
"dev": true,
"license": "ISC",
"dependencies": {
- "@conventional-changelog/template": "^1.2.1"
+ "@conventional-changelog/template": "^1.3.0"
},
"engines": {
"node": ">=22"
}
},
"node_modules/conventional-commits-parser": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-7.0.1.tgz",
- "integrity": "sha512-6VtskFpPsNkGVk/TY2RnV/MEdKfvCPBtQZN9x8jh9+k5RWBQ+tiaWn5UFCzTr0Dd88iKx7xghxbjBRp5uIzp3g==",
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-7.1.2.tgz",
+ "integrity": "sha512-O+x4N2yH+ijvqWlIyTHsXTAP+algNWgGbjY2duCe8w2vUMvUB95cLRslCPfTMQyLAKlet3bhZTdu6ozn4M+QJQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@simple-libs/stream-utils": "^2.0.0",
- "meow": "^14.0.0"
+ "argue-cli": "^3.1.0"
},
"bin": {
"conventional-commits-parser": "dist/cli/index.js"
@@ -4460,9 +4284,9 @@
"license": "CC0-1.0"
},
"node_modules/cytoscape": {
- "version": "3.34.0",
- "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.0.tgz",
- "integrity": "sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==",
+ "version": "3.34.1",
+ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.1.tgz",
+ "integrity": "sha512-Lr0RvH9H75y9ar8h9Toy6u4lxRSCcxUq+hHcQ26sVWo6BnaQp1gwEZOYqwuYTZhyW7npyKnNLP8oJ2p1/3OZ7g==",
"license": "MIT",
"engines": {
"node": ">=0.10"
@@ -5044,9 +4868,9 @@
}
},
"node_modules/devalue": {
- "version": "5.8.1",
- "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz",
- "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==",
+ "version": "5.9.0",
+ "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.0.tgz",
+ "integrity": "sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A==",
"license": "MIT"
},
"node_modules/devlop": {
@@ -5138,9 +4962,9 @@
}
},
"node_modules/dompurify": {
- "version": "3.4.11",
- "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz",
- "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==",
+ "version": "3.4.13",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz",
+ "integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
@@ -5281,9 +5105,9 @@
}
},
"node_modules/es-module-lexer": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz",
- "integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==",
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz",
+ "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==",
"license": "MIT"
},
"node_modules/es-object-atoms": {
@@ -5316,13 +5140,14 @@
}
},
"node_modules/es-toolkit": {
- "version": "1.49.0",
- "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.49.0.tgz",
- "integrity": "sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==",
+ "version": "1.50.0",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.50.0.tgz",
+ "integrity": "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==",
"license": "MIT",
"workspaces": [
"docs",
- "benchmarks"
+ "benchmarks",
+ "tests/types"
]
},
"node_modules/esast-util-from-estree": {
@@ -5358,9 +5183,9 @@
}
},
"node_modules/esbuild": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
- "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
+ "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
@@ -5370,32 +5195,32 @@
"node": ">=18"
},
"optionalDependencies": {
- "@esbuild/aix-ppc64": "0.28.1",
- "@esbuild/android-arm": "0.28.1",
- "@esbuild/android-arm64": "0.28.1",
- "@esbuild/android-x64": "0.28.1",
- "@esbuild/darwin-arm64": "0.28.1",
- "@esbuild/darwin-x64": "0.28.1",
- "@esbuild/freebsd-arm64": "0.28.1",
- "@esbuild/freebsd-x64": "0.28.1",
- "@esbuild/linux-arm": "0.28.1",
- "@esbuild/linux-arm64": "0.28.1",
- "@esbuild/linux-ia32": "0.28.1",
- "@esbuild/linux-loong64": "0.28.1",
- "@esbuild/linux-mips64el": "0.28.1",
- "@esbuild/linux-ppc64": "0.28.1",
- "@esbuild/linux-riscv64": "0.28.1",
- "@esbuild/linux-s390x": "0.28.1",
- "@esbuild/linux-x64": "0.28.1",
- "@esbuild/netbsd-arm64": "0.28.1",
- "@esbuild/netbsd-x64": "0.28.1",
- "@esbuild/openbsd-arm64": "0.28.1",
- "@esbuild/openbsd-x64": "0.28.1",
- "@esbuild/openharmony-arm64": "0.28.1",
- "@esbuild/sunos-x64": "0.28.1",
- "@esbuild/win32-arm64": "0.28.1",
- "@esbuild/win32-ia32": "0.28.1",
- "@esbuild/win32-x64": "0.28.1"
+ "@esbuild/aix-ppc64": "0.28.2",
+ "@esbuild/android-arm": "0.28.2",
+ "@esbuild/android-arm64": "0.28.2",
+ "@esbuild/android-x64": "0.28.2",
+ "@esbuild/darwin-arm64": "0.28.2",
+ "@esbuild/darwin-x64": "0.28.2",
+ "@esbuild/freebsd-arm64": "0.28.2",
+ "@esbuild/freebsd-x64": "0.28.2",
+ "@esbuild/linux-arm": "0.28.2",
+ "@esbuild/linux-arm64": "0.28.2",
+ "@esbuild/linux-ia32": "0.28.2",
+ "@esbuild/linux-loong64": "0.28.2",
+ "@esbuild/linux-mips64el": "0.28.2",
+ "@esbuild/linux-ppc64": "0.28.2",
+ "@esbuild/linux-riscv64": "0.28.2",
+ "@esbuild/linux-s390x": "0.28.2",
+ "@esbuild/linux-x64": "0.28.2",
+ "@esbuild/netbsd-arm64": "0.28.2",
+ "@esbuild/netbsd-x64": "0.28.2",
+ "@esbuild/openbsd-arm64": "0.28.2",
+ "@esbuild/openbsd-x64": "0.28.2",
+ "@esbuild/openharmony-arm64": "0.28.2",
+ "@esbuild/sunos-x64": "0.28.2",
+ "@esbuild/win32-arm64": "0.28.2",
+ "@esbuild/win32-ia32": "0.28.2",
+ "@esbuild/win32-x64": "0.28.2"
}
},
"node_modules/escalade": {
@@ -5561,15 +5386,15 @@
}
},
"node_modules/expressive-code": {
- "version": "0.44.0",
- "resolved": "https://registry.npmjs.org/expressive-code/-/expressive-code-0.44.0.tgz",
- "integrity": "sha512-JXVWVNCKlLuZLMQH8cOiDUSosT0Bb+elwE/dbAkpwFwDFmyFyWlECoWZIohh2FkIF1iI67TQJ+Ts9k7oNDh2qA==",
+ "version": "0.44.1",
+ "resolved": "https://registry.npmjs.org/expressive-code/-/expressive-code-0.44.1.tgz",
+ "integrity": "sha512-GakidxhapWDzpKLqEaFQ8wGk6gAqEtPQibu8+yPBfnDLgev5Vdsh1pasTxnrXL/mzIknyqeTwhMHTghdaiUrTg==",
"license": "MIT",
"dependencies": {
- "@expressive-code/core": "^0.44.0",
- "@expressive-code/plugin-frames": "^0.44.0",
- "@expressive-code/plugin-shiki": "^0.44.0",
- "@expressive-code/plugin-text-markers": "^0.44.0"
+ "@expressive-code/core": "^0.44.1",
+ "@expressive-code/plugin-frames": "^0.44.1",
+ "@expressive-code/plugin-shiki": "^0.44.1",
+ "@expressive-code/plugin-text-markers": "^0.44.1"
}
},
"node_modules/extend": {
@@ -5601,9 +5426,9 @@
}
},
"node_modules/fast-uri": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz",
- "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",
+ "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",
"dev": true,
"funding": [
{
@@ -5627,9 +5452,9 @@
}
},
"node_modules/fast-xml-builder": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.1.tgz",
- "integrity": "sha512-tPb5TTWfgfVx5BNSi2xV0eLr89POeXXn0dXIsCJ9m1narrWxeIyx6je9d7Rce/3NyXLbvuQmLkxq+RuxMWejvw==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.0.tgz",
+ "integrity": "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ==",
"funding": [
{
"type": "github",
@@ -5638,14 +5463,14 @@
],
"license": "MIT",
"dependencies": {
- "path-expression-matcher": "^1.5.0",
- "xml-naming": "^0.1.0"
+ "path-expression-matcher": "^1.6.2",
+ "xml-naming": "^0.3.0"
}
},
"node_modules/fast-xml-parser": {
- "version": "5.9.3",
- "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.9.3.tgz",
- "integrity": "sha512-brCNCeScma/kqa54J4PIDriSSSLssRkuYaUCpvHJulGc3HGI/xxKUCTDcYkAdqJsyb//ydpbxecjC3hB9+tb/g==",
+ "version": "5.10.1",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.10.1.tgz",
+ "integrity": "sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw==",
"funding": [
{
"type": "github",
@@ -5654,12 +5479,12 @@
],
"license": "MIT",
"dependencies": {
- "@nodable/entities": "^2.2.0",
+ "@nodable/entities": "^3.0.0",
"fast-xml-builder": "^1.2.0",
- "is-unsafe": "^1.0.1",
- "path-expression-matcher": "^1.5.0",
+ "is-unsafe": "^2.0.0",
+ "path-expression-matcher": "^1.6.2",
"strnum": "^2.4.1",
- "xml-naming": "^0.1.0"
+ "xml-naming": "^0.3.0"
},
"bin": {
"fxparser": "src/cli/cli.js"
@@ -6518,46 +6343,153 @@
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
"integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
"license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/html-whitespace-sensitive-tag-names": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-whitespace-sensitive-tag-names/-/html-whitespace-sensitive-tag-names-3.0.1.tgz",
+ "integrity": "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/http-cache-semantics": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
+ "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/httpsnippet": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/httpsnippet/-/httpsnippet-3.0.10.tgz",
+ "integrity": "sha512-1P102HsVslaT3IVfuUfVwxenfbogFiihqosnaKUScd/sON1omdZZCWdmzm6baRz4u1Va0CTI/7svLckCu9iNBw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.1.2",
+ "event-stream": "4.0.1",
+ "form-data": "4.0.4",
+ "har-validator-compiled": "^1.0.0",
+ "stringify-object": "3.3.0",
+ "yargs": "^17.4.0"
+ },
+ "bin": {
+ "httpsnippet": "bin/httpsnippet"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/httpsnippet/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/httpsnippet/node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/httpsnippet/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/httpsnippet/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/httpsnippet/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
}
},
- "node_modules/html-whitespace-sensitive-tag-names": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/html-whitespace-sensitive-tag-names/-/html-whitespace-sensitive-tag-names-3.0.1.tgz",
- "integrity": "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA==",
+ "node_modules/httpsnippet/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
"license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
- "node_modules/http-cache-semantics": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
- "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
- "license": "BSD-2-Clause"
- },
- "node_modules/httpsnippet": {
- "version": "3.0.10",
- "resolved": "https://registry.npmjs.org/httpsnippet/-/httpsnippet-3.0.10.tgz",
- "integrity": "sha512-1P102HsVslaT3IVfuUfVwxenfbogFiihqosnaKUScd/sON1omdZZCWdmzm6baRz4u1Va0CTI/7svLckCu9iNBw==",
+ "node_modules/httpsnippet/node_modules/yargs": {
+ "version": "17.7.3",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz",
+ "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "chalk": "^4.1.2",
- "event-stream": "4.0.1",
- "form-data": "4.0.4",
- "har-validator-compiled": "^1.0.0",
- "stringify-object": "3.3.0",
- "yargs": "^17.4.0"
- },
- "bin": {
- "httpsnippet": "bin/httpsnippet"
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
},
"engines": {
- "node": ">=20"
+ "node": ">=12"
+ }
+ },
+ "node_modules/httpsnippet/node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
}
},
"node_modules/human-signals": {
@@ -6587,9 +6519,9 @@
}
},
"node_modules/i18next": {
- "version": "26.3.4",
- "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.4.tgz",
- "integrity": "sha512-pa7m0d7pBDqGHZxljT+WPFeyFgQ7P7SciPPo1tTqYuO0z4sqADYhwnBESmmGp/wEof1inwdls/k8ZgTg8rxFHA==",
+ "version": "26.3.6",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.6.tgz",
+ "integrity": "sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA==",
"funding": [
{
"type": "individual",
@@ -6606,7 +6538,7 @@
],
"license": "MIT",
"peerDependencies": {
- "typescript": "^5 || ^6"
+ "typescript": "^5 || ^6 || ^7"
},
"peerDependenciesMeta": {
"typescript": {
@@ -6766,6 +6698,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-hexadecimal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
@@ -6845,9 +6787,9 @@
}
},
"node_modules/is-unsafe": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-1.0.1.tgz",
- "integrity": "sha512-CLK2+VdgERgD96EYm5lUQssZYlRg2tkZnbsxZoacmSiRxiFJ4Nk4SzjCl+Ur+v3kXIY9dTIdb3IH22y1mZ56LA==",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.0.tgz",
+ "integrity": "sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA==",
"funding": [
{
"type": "github",
@@ -6881,9 +6823,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
- "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
+ "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
"funding": [
{
"type": "github",
@@ -6998,9 +6940,9 @@
}
},
"node_modules/lightningcss": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
- "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
+ "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
@@ -7013,23 +6955,23 @@
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
- "lightningcss-android-arm64": "1.32.0",
- "lightningcss-darwin-arm64": "1.32.0",
- "lightningcss-darwin-x64": "1.32.0",
- "lightningcss-freebsd-x64": "1.32.0",
- "lightningcss-linux-arm-gnueabihf": "1.32.0",
- "lightningcss-linux-arm64-gnu": "1.32.0",
- "lightningcss-linux-arm64-musl": "1.32.0",
- "lightningcss-linux-x64-gnu": "1.32.0",
- "lightningcss-linux-x64-musl": "1.32.0",
- "lightningcss-win32-arm64-msvc": "1.32.0",
- "lightningcss-win32-x64-msvc": "1.32.0"
+ "lightningcss-android-arm64": "1.33.0",
+ "lightningcss-darwin-arm64": "1.33.0",
+ "lightningcss-darwin-x64": "1.33.0",
+ "lightningcss-freebsd-x64": "1.33.0",
+ "lightningcss-linux-arm-gnueabihf": "1.33.0",
+ "lightningcss-linux-arm64-gnu": "1.33.0",
+ "lightningcss-linux-arm64-musl": "1.33.0",
+ "lightningcss-linux-x64-gnu": "1.33.0",
+ "lightningcss-linux-x64-musl": "1.33.0",
+ "lightningcss-win32-arm64-msvc": "1.33.0",
+ "lightningcss-win32-x64-msvc": "1.33.0"
}
},
"node_modules/lightningcss-android-arm64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
- "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
+ "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
"cpu": [
"arm64"
],
@@ -7047,9 +6989,9 @@
}
},
"node_modules/lightningcss-darwin-arm64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
- "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
+ "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
"cpu": [
"arm64"
],
@@ -7067,9 +7009,9 @@
}
},
"node_modules/lightningcss-darwin-x64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
- "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
+ "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
"cpu": [
"x64"
],
@@ -7087,9 +7029,9 @@
}
},
"node_modules/lightningcss-freebsd-x64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
- "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
+ "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
"cpu": [
"x64"
],
@@ -7107,9 +7049,9 @@
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
- "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
+ "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
"cpu": [
"arm"
],
@@ -7127,9 +7069,9 @@
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
- "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
+ "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
"cpu": [
"arm64"
],
@@ -7150,9 +7092,9 @@
}
},
"node_modules/lightningcss-linux-arm64-musl": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
- "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
+ "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
"cpu": [
"arm64"
],
@@ -7173,9 +7115,9 @@
}
},
"node_modules/lightningcss-linux-x64-gnu": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
- "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
+ "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
"cpu": [
"x64"
],
@@ -7196,9 +7138,9 @@
}
},
"node_modules/lightningcss-linux-x64-musl": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
- "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
+ "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
"cpu": [
"x64"
],
@@ -7219,9 +7161,9 @@
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
- "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
+ "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
"cpu": [
"arm64"
],
@@ -7239,9 +7181,9 @@
}
},
"node_modules/lightningcss-win32-x64-msvc": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
- "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
+ "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
"cpu": [
"x64"
],
@@ -7266,9 +7208,9 @@
"license": "MIT"
},
"node_modules/lint-staged": {
- "version": "17.2.0",
- "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.2.0.tgz",
- "integrity": "sha512-FchGnFe4i4B1C/a35SPU9bNGPEHSC1+1iV0plLjzBmKVe9klZrlRfSgK6Cw4VeHyqOXbJUXP0vON61uRftNQ0A==",
+ "version": "17.3.0",
+ "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.3.0.tgz",
+ "integrity": "sha512-woZS3vNe3UKqBaLPvbLOtKRY4tLANpWQhom12MGWqC8Mh1lCOO+WgSwmX2amjJAqTY9BkXYW87fCUH5H9Ph6xw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -7306,9 +7248,9 @@
}
},
"node_modules/lru-cache": {
- "version": "11.5.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz",
- "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==",
+ "version": "11.5.2",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
+ "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
"license": "BlueOak-1.0.0",
"engines": {
"node": "20 || >=22"
@@ -7324,13 +7266,13 @@
}
},
"node_modules/magicast": {
- "version": "0.5.3",
- "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz",
- "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==",
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.4.tgz",
+ "integrity": "sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==",
"license": "MIT",
"dependencies": {
- "@babel/parser": "^7.29.3",
- "@babel/types": "^7.29.0",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7",
"source-map-js": "^1.2.1"
}
},
@@ -7714,23 +7656,10 @@
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
"license": "CC0-1.0"
},
- "node_modules/meow": {
- "version": "14.1.0",
- "resolved": "https://registry.npmjs.org/meow/-/meow-14.1.0.tgz",
- "integrity": "sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=20"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/mermaid": {
- "version": "11.16.0",
- "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.16.0.tgz",
- "integrity": "sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA==",
+ "version": "11.16.1",
+ "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.16.1.tgz",
+ "integrity": "sha512-TQsq6u22fAn3rek5VOubrhKPo1g5hwC3FXUN9hiyupTckcYiGuuKGkNQrKYwGJkXUxZdojwRG46gsSCFZMDp4g==",
"license": "MIT",
"dependencies": {
"@braintree/sanitize-url": "^7.1.2",
@@ -8565,9 +8494,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
- "version": "3.3.15",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
- "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
+ "version": "3.3.18",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
+ "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
"funding": [
{
"type": "github",
@@ -8611,9 +8540,9 @@
"license": "MIT"
},
"node_modules/node-mock-http": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz",
- "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==",
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.5.tgz",
+ "integrity": "sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw==",
"license": "MIT"
},
"node_modules/normalize-path": {
@@ -8668,9 +8597,9 @@
}
},
"node_modules/obug": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz",
- "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==",
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz",
+ "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==",
"funding": [
"https://github.com/sponsors/sxzz",
"https://opencollective.com/debug"
@@ -8723,9 +8652,9 @@
"peer": true
},
"node_modules/p-limit": {
- "version": "7.3.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.0.tgz",
- "integrity": "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==",
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.1.tgz",
+ "integrity": "sha512-0trZaiG7Y7kN/Egy9a8j47t9osC0Tch4PaIWd9yGF6bvmlk7muExRvGNYb8sXBwEKMoNKsbNN9P8EefuQekE4Q==",
"license": "MIT",
"dependencies": {
"yocto-queue": "^1.2.1"
@@ -8738,9 +8667,9 @@
}
},
"node_modules/p-queue": {
- "version": "9.3.1",
- "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.1.tgz",
- "integrity": "sha512-POWdiIPmsUPGwb4FeQ4OBg46aqmcInSWe45CKDsGHiOBiVQM9chqfQTuqhuTzcg2Vz9faTI65at0KkVyVEiCHw==",
+ "version": "9.3.3",
+ "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.3.tgz",
+ "integrity": "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA==",
"license": "MIT",
"dependencies": {
"eventemitter3": "^5.0.4",
@@ -8766,9 +8695,9 @@
}
},
"node_modules/package-manager-detector": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.7.0.tgz",
- "integrity": "sha512-xg1eHpwYL/D/HEdWw2goFZP6vV0FH7W+PZ5rFkGjdIDLtxq7EkzBUeT3m+lndYCt8wKbmofUu1MUdMCXkCk9ZQ==",
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz",
+ "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==",
"license": "MIT"
},
"node_modules/pagefind": {
@@ -8903,9 +8832,9 @@
"license": "MIT"
},
"node_modules/path-expression-matcher": {
- "version": "1.6.1",
- "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.1.tgz",
- "integrity": "sha512-h7bxdzhHk8Knyc4Tj+jMaa7fEEoUJy7p1qtbVgkYg1Uhpe5Np5VuGXCRZnkZvU+Q42M1vStt0ifa3ueykRJPmQ==",
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz",
+ "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==",
"funding": [
{
"type": "github",
@@ -8981,9 +8910,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.16",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
- "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
+ "version": "8.5.26",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
+ "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
"funding": [
{
"type": "opencollective",
@@ -9000,7 +8929,7 @@
],
"license": "MIT",
"dependencies": {
- "nanoid": "^3.3.12",
+ "nanoid": "^3.3.17",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -9249,12 +9178,12 @@
}
},
"node_modules/rehype-expressive-code": {
- "version": "0.44.0",
- "resolved": "https://registry.npmjs.org/rehype-expressive-code/-/rehype-expressive-code-0.44.0.tgz",
- "integrity": "sha512-5r74C5F2sMR3X+QJH8OKWgZBO/cqRw5W1fLT6GVlSfLqepk+4j8tGFkyPqZYWjwntsBHzKPDH2zI68sZ7ScLfA==",
+ "version": "0.44.1",
+ "resolved": "https://registry.npmjs.org/rehype-expressive-code/-/rehype-expressive-code-0.44.1.tgz",
+ "integrity": "sha512-+VZgs7Evw4LXRN3owpoBNSTpYuW6GeOdjqcUT1TuY8o/4MGPtbd0EU7Bgrju7X8KrQ6SslOBAuGWJ5fV5TriJQ==",
"license": "MIT",
"dependencies": {
- "expressive-code": "^0.44.0"
+ "expressive-code": "^0.44.1"
}
},
"node_modules/rehype-format": {
@@ -9446,9 +9375,9 @@
}
},
"node_modules/remark-smartypants": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-3.0.2.tgz",
- "integrity": "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==",
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-3.0.3.tgz",
+ "integrity": "sha512-gCaK+ndZ0hYezlqFegHFCVh2CQemsi0Npdh1qVM9bxlUFknjkbP6VmojWhddOCrbK0PbbacmYLWfTULRiT1eWA==",
"license": "MIT",
"dependencies": {
"retext": "^9.0.0",
@@ -9589,12 +9518,12 @@
"license": "Unlicense"
},
"node_modules/rolldown": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz",
- "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.4.tgz",
+ "integrity": "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==",
"license": "MIT",
"dependencies": {
- "@oxc-project/types": "=0.138.0",
+ "@oxc-project/types": "=0.144.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
@@ -9604,21 +9533,20 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
- "@rolldown/binding-android-arm64": "1.1.4",
- "@rolldown/binding-darwin-arm64": "1.1.4",
- "@rolldown/binding-darwin-x64": "1.1.4",
- "@rolldown/binding-freebsd-x64": "1.1.4",
- "@rolldown/binding-linux-arm-gnueabihf": "1.1.4",
- "@rolldown/binding-linux-arm64-gnu": "1.1.4",
- "@rolldown/binding-linux-arm64-musl": "1.1.4",
- "@rolldown/binding-linux-ppc64-gnu": "1.1.4",
- "@rolldown/binding-linux-s390x-gnu": "1.1.4",
- "@rolldown/binding-linux-x64-gnu": "1.1.4",
- "@rolldown/binding-linux-x64-musl": "1.1.4",
- "@rolldown/binding-openharmony-arm64": "1.1.4",
- "@rolldown/binding-wasm32-wasi": "1.1.4",
- "@rolldown/binding-win32-arm64-msvc": "1.1.4",
- "@rolldown/binding-win32-x64-msvc": "1.1.4"
+ "@rolldown/binding-android-arm64": "1.2.4",
+ "@rolldown/binding-darwin-arm64": "1.2.4",
+ "@rolldown/binding-darwin-x64": "1.2.4",
+ "@rolldown/binding-freebsd-x64": "1.2.4",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.2.4",
+ "@rolldown/binding-linux-arm64-gnu": "1.2.4",
+ "@rolldown/binding-linux-arm64-musl": "1.2.4",
+ "@rolldown/binding-linux-ppc64-gnu": "1.2.4",
+ "@rolldown/binding-linux-s390x-gnu": "1.2.4",
+ "@rolldown/binding-linux-x64-gnu": "1.2.4",
+ "@rolldown/binding-linux-x64-musl": "1.2.4",
+ "@rolldown/binding-openharmony-arm64": "1.2.4",
+ "@rolldown/binding-win32-arm64-msvc": "1.2.4",
+ "@rolldown/binding-win32-x64-msvc": "1.2.4"
}
},
"node_modules/roughjs": {
@@ -9663,9 +9591,10 @@
}
},
"node_modules/satteri": {
- "version": "0.9.4",
- "resolved": "https://registry.npmjs.org/satteri/-/satteri-0.9.4.tgz",
- "integrity": "sha512-BKob126Tay84diOZsnVNH/Q/c+3njPJTCad3w5zLKa6j8bVjxskPNHDtxrMwYK4bN/RlqUSdMnPwKY4k65EMOQ==",
+ "version": "0.9.5",
+ "resolved": "https://registry.npmjs.org/satteri/-/satteri-0.9.5.tgz",
+ "integrity": "sha512-ZuWVl+vnM64y+/TtX8Kosv2c00W+hLQiiwnEL6H0UKVVrxFqMw4D2CJHHQaouVd89OAhtBBfjWLqhKi3TVUV4w==",
+ "license": "MIT",
"dependencies": {
"@types/estree-jsx": "^1.0.5",
"@types/hast": "^3.0.4",
@@ -9673,21 +9602,21 @@
"@types/unist": "^3.0.3"
},
"optionalDependencies": {
- "@bruits/satteri-darwin-arm64": "0.9.4",
- "@bruits/satteri-darwin-x64": "0.9.4",
- "@bruits/satteri-linux-arm64-gnu": "0.9.4",
- "@bruits/satteri-linux-arm64-musl": "0.9.4",
- "@bruits/satteri-linux-x64-gnu": "0.9.4",
- "@bruits/satteri-linux-x64-musl": "0.9.4",
- "@bruits/satteri-wasm32-wasi": "0.9.4",
- "@bruits/satteri-win32-arm64-msvc": "0.9.4",
- "@bruits/satteri-win32-x64-msvc": "0.9.4"
+ "@bruits/satteri-darwin-arm64": "0.9.5",
+ "@bruits/satteri-darwin-x64": "0.9.5",
+ "@bruits/satteri-linux-arm64-gnu": "0.9.5",
+ "@bruits/satteri-linux-arm64-musl": "0.9.5",
+ "@bruits/satteri-linux-x64-gnu": "0.9.5",
+ "@bruits/satteri-linux-x64-musl": "0.9.5",
+ "@bruits/satteri-wasm32-wasi": "0.9.5",
+ "@bruits/satteri-win32-arm64-msvc": "0.9.5",
+ "@bruits/satteri-win32-x64-msvc": "0.9.5"
}
},
"node_modules/sax": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
- "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz",
+ "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==",
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=11.0.0"
@@ -9799,19 +9728,19 @@
}
},
"node_modules/shiki": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.3.1.tgz",
- "integrity": "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw==",
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.4.3.tgz",
+ "integrity": "sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g==",
"license": "MIT",
"dependencies": {
- "@shikijs/core": "4.3.1",
- "@shikijs/engine-javascript": "4.3.1",
- "@shikijs/engine-oniguruma": "4.3.1",
- "@shikijs/langs": "4.3.1",
- "@shikijs/themes": "4.3.1",
- "@shikijs/types": "4.3.1",
+ "@shikijs/core": "4.4.3",
+ "@shikijs/engine-javascript": "4.4.3",
+ "@shikijs/engine-oniguruma": "4.4.3",
+ "@shikijs/langs": "4.4.3",
+ "@shikijs/themes": "4.4.3",
+ "@shikijs/types": "4.4.3",
"@shikijs/vscode-textmate": "^10.0.2",
- "@types/hast": "^3.0.4"
+ "@types/hast": "^3.0.5"
},
"engines": {
"node": ">=20"
@@ -9856,9 +9785,9 @@
}
},
"node_modules/sitemap/node_modules/@types/node": {
- "version": "24.13.2",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz",
- "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==",
+ "version": "24.13.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz",
+ "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.18.0"
@@ -9871,9 +9800,9 @@
"license": "MIT"
},
"node_modules/smol-toml": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.0.tgz",
- "integrity": "sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==",
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.8.0.tgz",
+ "integrity": "sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ==",
"license": "BSD-3-Clause",
"engines": {
"node": ">= 18"
@@ -9958,9 +9887,9 @@
}
},
"node_modules/starlight-links-validator": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/starlight-links-validator/-/starlight-links-validator-0.25.2.tgz",
- "integrity": "sha512-RQiHkM8FHKermsjMkSb+uS/q50Dv5P0O0ECWKxJyTv4stuO8QTorIEKnITDDLEf9/xyg7M69arxiK2ola3OMqA==",
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/starlight-links-validator/-/starlight-links-validator-0.25.3.tgz",
+ "integrity": "sha512-kNE2F8fwdq7r8l6Zx2nH3SDnLlVu2VRmbmyV/nSpbtWzf9+S81H2VBZ6GRevIKbm8qOoYSZvXzNATT8ZXw9bmw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -10058,6 +9987,23 @@
"node": ">=0.6.19"
}
},
+ "node_modules/string-width": {
+ "version": "8.2.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz",
+ "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-east-asian-width": "^1.5.0",
+ "strip-ansi": "^7.1.2"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/stringify-entities": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
@@ -10222,9 +10168,9 @@
}
},
"node_modules/svgo": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.1.tgz",
- "integrity": "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==",
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.2.tgz",
+ "integrity": "sha512-ekx94z1rRc5LDi6oSUaeRnYhd0UOJxdtQCL2rF8xpWxD3TPAsISWOrxezqGovqS38GRZOdpDfvQe3ts6F7nsng==",
"license": "MIT",
"dependencies": {
"commander": "^11.1.0",
@@ -10295,9 +10241,9 @@
}
},
"node_modules/tinyexec": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
- "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz",
+ "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==",
"license": "MIT",
"engines": {
"node": ">=18"
@@ -10380,9 +10326,9 @@
"optional": true
},
"node_modules/tsx": {
- "version": "4.23.1",
- "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz",
- "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==",
+ "version": "4.23.12",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz",
+ "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -10435,9 +10381,9 @@
"license": "MIT"
},
"node_modules/ultrahtml": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.6.0.tgz",
- "integrity": "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==",
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.7.0.tgz",
+ "integrity": "sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g==",
"license": "MIT"
},
"node_modules/uncrypto": {
@@ -10446,6 +10392,15 @@
"integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==",
"license": "MIT"
},
+ "node_modules/undici": {
+ "version": "8.10.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz",
+ "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=22.19.0"
+ }
+ },
"node_modules/undici-types": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
@@ -10485,14 +10440,14 @@
}
},
"node_modules/unifont": {
- "version": "0.7.4",
- "resolved": "https://registry.npmjs.org/unifont/-/unifont-0.7.4.tgz",
- "integrity": "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg==",
+ "version": "0.7.5",
+ "resolved": "https://registry.npmjs.org/unifont/-/unifont-0.7.5.tgz",
+ "integrity": "sha512-ULe/Cs+ZIsq+dcFofNkhqielCrUJnb5mr+Yc4EBM2VlL+6OZR6+cjtI2mT1bJvRBrVncqHAbLURxmPLcCXzWMg==",
"license": "MIT",
"dependencies": {
"css-tree": "^3.1.0",
- "ofetch": "^1.5.1",
- "ohash": "^2.0.11"
+ "ohash": "^2.0.11",
+ "undici": "^8.0.0"
}
},
"node_modules/unist-util-find-after": {
@@ -10758,9 +10713,9 @@
}
},
"node_modules/unstorage/node_modules/readdirp": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
- "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz",
+ "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==",
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
@@ -10854,15 +10809,15 @@
}
},
"node_modules/vite": {
- "version": "8.1.3",
- "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz",
- "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==",
+ "version": "8.2.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz",
+ "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==",
"license": "MIT",
"dependencies": {
- "lightningcss": "^1.32.0",
- "picomatch": "^4.0.4",
- "postcss": "^8.5.16",
- "rolldown": "~1.1.3",
+ "lightningcss": "^1.33.0",
+ "picomatch": "^4.0.5",
+ "postcss": "^8.5.25",
+ "rolldown": "~1.2.1",
"tinyglobby": "^0.2.17"
},
"bin": {
@@ -10879,7 +10834,7 @@
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
- "@vitejs/devtools": "^0.3.0",
+ "@vitejs/devtools": "^0.4.0",
"esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
@@ -11258,10 +11213,59 @@
"node": ">= 8"
}
},
+ "node_modules/wrap-ansi": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
+ "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/xml-naming": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz",
- "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==",
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz",
+ "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==",
"funding": [
{
"type": "github",
@@ -11352,22 +11356,21 @@
}
},
"node_modules/yargs": {
- "version": "17.7.3",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz",
- "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==",
+ "version": "18.1.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.1.0.tgz",
+ "integrity": "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "cliui": "^8.0.1",
+ "cliui": "^9.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
- "require-directory": "^2.1.1",
- "string-width": "^4.2.3",
+ "string-width": "^8.2.1",
"y18n": "^5.0.5",
- "yargs-parser": "^21.1.1"
+ "yargs-parser": "^22.0.0"
},
"engines": {
- "node": ">=12"
+ "node": "^20.19.0 || ^22.12.0 || >=23"
}
},
"node_modules/yargs-parser": {
@@ -11379,71 +11382,6 @@
"node": "^20.19.0 || ^22.12.0 || >=23"
}
},
- "node_modules/yargs/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/yargs/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/yargs/node_modules/is-fullwidth-code-point": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/yargs/node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/yargs/node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/yargs/node_modules/yargs-parser": {
- "version": "21.1.1",
- "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
- "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/yocto-queue": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz",
@@ -11457,9 +11395,9 @@
}
},
"node_modules/yoctocolors": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz",
- "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz",
+ "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==",
"dev": true,
"license": "MIT",
"engines": {
From 71dd92afdf35f5f80d2885de48d02623bc1969f6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
Date: Wed, 12 Aug 2026 23:10:55 +0200
Subject: [PATCH 36/45] fix: release hardening before v0.1.0 (#46, #47, #49,
#50) (#75)
---
.gitmodules | 4 ----
astro.config.mjs | 8 +++++++
github-profile | 1 -
package-lock.json | 5 ++---
package.json | 6 ++---
scripts/compile.test.ts | 28 +++++++++++++++++++++++
scripts/compile.ts | 23 +++++++++----------
scripts/source-schema.ts | 23 ++++++++++++++++++-
src/lib/noindex.test.ts | 40 +++++++++++++++++++++++++++++++++
src/lib/noindex.ts | 48 ++++++++++++++++++++++++++++++++++++++++
10 files changed, 161 insertions(+), 25 deletions(-)
delete mode 160000 github-profile
create mode 100644 src/lib/noindex.test.ts
create mode 100644 src/lib/noindex.ts
diff --git a/.gitmodules b/.gitmodules
index b977b73..3889a1b 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -2,7 +2,3 @@
path = data
url = https://github.com/textrefs/registry.git
branch = main
-[submodule "github-profile"]
- path = github-profile
- url = https://github.com/textrefs/.github.git
- branch = main
diff --git a/astro.config.mjs b/astro.config.mjs
index f3e085a..d15dd30 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -6,16 +6,24 @@ import starlightLinksValidator from 'starlight-links-validator';
import starlightLlmsTxt from 'starlight-llms-txt';
import starlightOpenAPI, { openAPISidebarGroups } from 'starlight-openapi';
import starlightBlog from 'starlight-blog';
+import sitemap from '@astrojs/sitemap';
+import { buildNoindexPredicate } from './src/lib/noindex.ts';
const siteDomain = process.env.SITE_DOMAIN ?? 'textrefs.org';
const site = siteDomain.startsWith('http')
? siteDomain
: `https://${siteDomain}`;
+const isNoindex = buildNoindexPredicate();
+
export default defineConfig({
site,
integrations: [
mermaid({ autoTheme: true }),
+ // Declared explicitly so the sitemap can drop draft record pages;
+ // Starlight adds `@astrojs/sitemap` with default options only when the
+ // project has not already registered it.
+ sitemap({ filter: (page) => !isNoindex(new URL(page).pathname) }),
starlight({
plugins: [
starlightBlog({
diff --git a/github-profile b/github-profile
deleted file mode 160000
index 9b707b9..0000000
--- a/github-profile
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 9b707b984843736e0d917aea1060d1def560634e
diff --git a/package-lock.json b/package-lock.json
index a431bef..fc0c6f9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,11 +9,13 @@
"version": "0.1.0",
"license": "AGPL-3.0-or-later",
"dependencies": {
+ "@astrojs/sitemap": "^3.7.3",
"@astrojs/starlight": "^0.41.4",
"astro": "^7.1.3",
"astro-mermaid": "^2.1.0",
"mermaid": "^11.15.0",
"sharp": "^0.35.3",
+ "spdx-license-ids": "^3.0.23",
"starlight-blog": "^0.28.0",
"uuid": "^14.0.0",
"yaml": "^2.9.0",
@@ -21,7 +23,6 @@
},
"devDependencies": {
"@astrojs/check": "^0.9.9",
- "@astrojs/markdown-satteri": "^0.3.4",
"@commitlint/cli": "^21.2.1",
"@commitlint/config-conventional": "^21.0.1",
"git-cliff": "^2.13.1",
@@ -30,7 +31,6 @@
"pagefind": "^1.5.2",
"prettier": "^3.9.6",
"prettier-plugin-astro": "^0.14.1",
- "spdx-license-ids": "^3.0.23",
"starlight-links-validator": "^0.25.2",
"starlight-llms-txt": "^0.11.0",
"starlight-openapi": "^0.26.0",
@@ -9843,7 +9843,6 @@
"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": {
diff --git a/package.json b/package.json
index b847608..769f3c4 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,7 @@
"astro": "astro",
"check": "astro check",
"check:fast": "TEXTREFS_REGISTRY_FIXTURE=1 astro check",
- "test": "tsx --test scripts/*.test.ts",
+ "test": "tsx --test scripts/*.test.ts src/lib/*.test.ts",
"compile:data": "tsx scripts/compile.ts",
"build:data": "npm run compile:data && npm run validate:data",
"validate:data": "tsx scripts/validate-data.ts",
@@ -29,11 +29,13 @@
"*.{astro,ts,js,mjs,cjs,css,md,mdx,json,yml,yaml}": "prettier --write"
},
"dependencies": {
+ "@astrojs/sitemap": "^3.7.3",
"@astrojs/starlight": "^0.41.4",
"astro": "^7.1.3",
"astro-mermaid": "^2.1.0",
"mermaid": "^11.15.0",
"sharp": "^0.35.3",
+ "spdx-license-ids": "^3.0.23",
"starlight-blog": "^0.28.0",
"uuid": "^14.0.0",
"yaml": "^2.9.0",
@@ -41,7 +43,6 @@
},
"devDependencies": {
"@astrojs/check": "^0.9.9",
- "@astrojs/markdown-satteri": "^0.3.4",
"@commitlint/cli": "^21.2.1",
"@commitlint/config-conventional": "^21.0.1",
"git-cliff": "^2.13.1",
@@ -50,7 +51,6 @@
"pagefind": "^1.5.2",
"prettier": "^3.9.6",
"prettier-plugin-astro": "^0.14.1",
- "spdx-license-ids": "^3.0.23",
"starlight-links-validator": "^0.25.2",
"starlight-llms-txt": "^0.11.0",
"starlight-openapi": "^0.26.0",
diff --git a/scripts/compile.test.ts b/scripts/compile.test.ts
index eb7e7f2..b6a70f8 100644
--- a/scripts/compile.test.ts
+++ b/scripts/compile.test.ts
@@ -522,6 +522,34 @@ references:
assert.equal(reg.warnings, 1);
});
+// --- Resolver licences -------------------------------------------------------
+
+test('an SPDX license id becomes the canonical SPDX IRI', () => {
+ const reg = compileFixture(
+ workWithResolver(` - url: 'https://example.org/{book}/{chapter}'
+ license: CC-BY-4.0`),
+ );
+ assert.equal(
+ reg.references.find((r) => r.locator === 'Gen.1')?.resolver_targets[0]
+ ?.license,
+ 'https://spdx.org/licenses/CC-BY-4.0',
+ );
+});
+
+test('a non-SPDX license id fails the build instead of dropping the licence', (t) => {
+ const logged: string[] = [];
+ t.mock.method(console, 'error', (...args: unknown[]) => {
+ logged.push(args.join(' '));
+ });
+ assert.throws(() =>
+ compileFixture(
+ workWithResolver(` - url: 'https://example.org/{book}/{chapter}'
+ license: CC-BY-4`),
+ ),
+ );
+ assert.match(logged.join('\n'), /not an SPDX license id/);
+});
+
test('url_by with more than one selector variable is rejected at parse time', (t) => {
// The thrown message only names the file; the failing rule goes to
// console.error, so capture it rather than assert on any parse error.
diff --git a/scripts/compile.ts b/scripts/compile.ts
index fa60b79..d7b95a1 100644
--- a/scripts/compile.ts
+++ b/scripts/compile.ts
@@ -9,7 +9,6 @@ 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,
@@ -24,17 +23,13 @@ import {
type ReferenceRangeSource as ReferenceRange,
type ReferenceSource,
type ResolverEntrySource as ResolverEntry,
+ SPDX_IDS,
type SystemBlockSource,
} from './source-schema.js';
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');
@@ -281,15 +276,17 @@ function buildResolverEntry(
if (resolver.provider !== undefined) entry.provider = resolver.provider;
entry.access = resolver.access ?? 'unknown';
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 (!SPDX_IDS.has(resolver.license)) {
+ // Unreachable via authored YAML — ResolverEntrySource rejects it at
+ // parse time. Kept so a future caller that skips the parser cannot
+ // drop a licence statement silently.
+ throw new Error(
+ `license "${resolver.license}" is not an SPDX id (use license_url for non-SPDX terms)`,
);
}
+ // 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}`;
}
if (resolver.license_url !== undefined)
entry.license_url = resolver.license_url;
diff --git a/scripts/source-schema.ts b/scripts/source-schema.ts
index 5d2b0f7..c96ea32 100644
--- a/scripts/source-schema.ts
+++ b/scripts/source-schema.ts
@@ -4,9 +4,20 @@
// authoritative — see `standard/schema/` — but a typo in source YAML used to
// slip through an unchecked `as WorkSource` cast and surface much later, or not
// at all. Objects are strict: an unknown key is an authoring error.
+import { createRequire } from 'node:module';
import { z } from 'zod';
import { FlatKey, IsoDate, Status } from '../standard/schema/common.js';
+const require = createRequire(import.meta.url);
+const spdxLicenseIds: string[] = require('spdx-license-ids');
+const spdxDeprecatedIds: string[] = require('spdx-license-ids/deprecated');
+
+/** Current and deprecated SPDX identifiers, the only accepted `license` values. */
+export const SPDX_IDS = new Set([
+ ...spdxLicenseIds,
+ ...spdxDeprecatedIds,
+]);
+
export const ResolverEntrySource = z
.strictObject({
url: z.string().min(1).optional(),
@@ -30,7 +41,17 @@ export const ResolverEntrySource = z
edition: z.string().min(1).optional(),
language: z.string().min(2).optional(),
access: z.enum(['open', 'paywalled', 'restricted', 'unknown']).optional(),
- license: z.string().min(1).optional(),
+ // Checked here rather than at emit time: the compiler maps an id to its
+ // canonical SPDX IRI, and anything it cannot map would otherwise drop out
+ // of the published record silently. A typo must fail the build, not
+ // remove the licence statement.
+ license: z
+ .string()
+ .min(1)
+ .refine((id) => SPDX_IDS.has(id), {
+ message: 'not an SPDX license id (use license_url for non-SPDX terms)',
+ })
+ .optional(),
license_url: z.string().min(1).optional(),
last_checked: IsoDate.optional(),
})
diff --git a/src/lib/noindex.test.ts b/src/lib/noindex.test.ts
new file mode 100644
index 0000000..60096be
--- /dev/null
+++ b/src/lib/noindex.test.ts
@@ -0,0 +1,40 @@
+// The sitemap filter in `astro.config.mjs` is the third leg of ADR-0003's
+// "noindex, excluded from sitemap and search" rule. It runs against the fixture
+// registry here, which carries both `active` and `draft` records — the real
+// registry is entirely `draft` under ADR-0004, so it cannot show that the
+// predicate discriminates rather than excluding everything under `/id/`.
+process.env.TEXTREFS_REGISTRY_FIXTURE = '1';
+
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { buildNoindexPredicate } from './noindex.js';
+
+const isNoindex = buildNoindexPredicate();
+
+test('draft record pages are excluded', () => {
+ assert.equal(isNoindex('/id/system/fixture-alternate/'), true);
+ assert.equal(
+ isNoindex('/id/ref/00000000-0000-5000-8000-000000000003/'),
+ true,
+ );
+});
+
+test('active record pages are kept', () => {
+ assert.equal(isNoindex('/id/work/fixture.work/'), false);
+ assert.equal(isNoindex('/id/system/fixture-section/'), false);
+ assert.equal(
+ isNoindex('/id/ref/00000000-0000-5000-8000-000000000001/'),
+ false,
+ );
+});
+
+test('documentation pages are kept', () => {
+ assert.equal(isNoindex('/'), false);
+ assert.equal(isNoindex('/standard/specification/'), false);
+ assert.equal(isNoindex('/reg/'), false);
+});
+
+test('paginated reference browsers follow their work', () => {
+ // fixture.work is active, so its browser pages stay in the sitemap.
+ assert.equal(isNoindex('/reg/work/fixture.work/refs/1/'), false);
+});
diff --git a/src/lib/noindex.ts b/src/lib/noindex.ts
new file mode 100644
index 0000000..3462511
--- /dev/null
+++ b/src/lib/noindex.ts
@@ -0,0 +1,48 @@
+// ADR-0003 (as amended by ADR-0004) says draft record pages are rendered
+// `noindex`, excluded from search, and excluded from the sitemap. The first two
+// ship in the record templates; this module supplies the third.
+//
+// The predicate is derived from record status rather than from a route prefix,
+// so it narrows on its own as records are promoted to `active` — the alternative
+// (excluding `/id/` wholesale) would keep hiding pages that had earned a place
+// in the sitemap.
+import {
+ loadWorks,
+ loadSystems,
+ loadReferences,
+ loadMappings,
+ iriToLocal,
+ workKeyOf,
+} from './registry.js';
+
+function isDraft(record: { status: string }): boolean {
+ return record.status === 'draft';
+}
+
+/**
+ * A predicate over pathnames that is true for pages rendered `noindex`.
+ *
+ * Built once and reused: the record set is large (one path per reference), so
+ * membership is an exact `Set` lookup. Paginated reference browsers live under
+ * `/reg/work/{key}/refs/{n}/` and follow their work's status, which is a prefix
+ * test over the handful of works rather than an enumeration of page numbers.
+ */
+export function buildNoindexPredicate(): (pathname: string) => boolean {
+ const paths = new Set();
+ for (const record of [
+ ...loadWorks(),
+ ...loadSystems(),
+ ...loadReferences(),
+ ...loadMappings(),
+ ]) {
+ if (isDraft(record)) paths.add(iriToLocal(record.id));
+ }
+
+ const draftWorkPrefixes = loadWorks()
+ .filter(isDraft)
+ .map((work) => `/reg/work/${workKeyOf(work.id)}/`);
+
+ return (pathname) =>
+ paths.has(pathname) ||
+ draftWorkPrefixes.some((prefix) => pathname.startsWith(prefix));
+}
From 0df2b27824f824caf2e55324718d7cdf10852e24 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
Date: Wed, 12 Aug 2026 23:17:39 +0200
Subject: [PATCH 37/45] =?UTF-8?q?chore(release):=20v0.1.0=20metadata=20?=
=?UTF-8?q?=E2=80=94=20CITATION.cff,=20changelog,=20checklist=20fixes=20(#?=
=?UTF-8?q?76)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
CHANGELOG.md | 14 ++++++++++++--
CITATION.cff | 2 ++
CONTRIBUTING.md | 16 +++++++++-------
src/content/docs/standard/versioning.md | 5 +++--
4 files changed, 26 insertions(+), 11 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cd8b9f1..01ca313 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-26
+## [0.1.0] - 2026-08-12
### Added
@@ -13,6 +13,10 @@ All notable changes to this project are documented in this file. The format is b
- 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)
+- Collapse record lifecycle to draft → active (ADR-0004) (#62)
+- Preferred citation system and qualified /cite/ aliases (ADR-0005) (#63)
+- Replace SKOS mapping relations with alternateOf and isReferencedBy (ADR-0006) (#67)
+- Map locator variables into a provider's own vocabulary (#71) (#72)
### Documentation
@@ -28,7 +32,10 @@ All notable changes to this project are documented in this file. The format is b
- Mark docs/404.mdx as draft to drop catch-all route conflict
- Repair link-check workflow + bump deps to zod 4 (#5)
- Erratum batch and spec-consistency fixes (#10, #11, #12, #14) (#25)
-- Resolve normative contradictions before the v0.1.0 tag
+- Resolve normative contradictions before the v0.1.0 tag (#44)
+- Add explicit whitespace between inline elements (#53) (#54)
+- List works instead of references on CitationSystem pages (#55) (#56)
+- Release hardening before v0.1.0 (#46, #47, #49, #50) (#75)
### Misc
@@ -40,3 +47,6 @@ All notable changes to this project are documented in this file. The format is b
- Astro 6.4.8, dompurify 3.4.11, actions/checkout v7
- Bump submodule to registry main (c0a3275, ORCID docs)
- Astro 7.1 + dependency refresh (#39) (#43)
+- Bump registry pin to the ADR-0006 reclassification
+- Bump registry pin to the resolver review
+- Refresh all dependencies before v0.1.0 (#73)
diff --git a/CITATION.cff b/CITATION.cff
index ee1ee82..b56d6e8 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -1,6 +1,8 @@
cff-version: 1.2.0
message: 'If you cite TextRefs, please cite it as below.'
title: TextRefs
+version: 0.1.0
+date-released: '2026-08-12'
abstract: An open, persistent, machine-readable registry for canonical text references.
type: software
authors:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 041b045..30e92d9 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -122,13 +122,15 @@ Two release trains. The Zenodo–GitHub webhook MUST be enabled once per reposit
**Standard + site** (this repo):
-1. Bump `version` in `package.json` to match the new tag.
-2. `npm run changelog` to regenerate `CHANGELOG.md`.
-3. Update spec page frontmatter `maturity:` if the release transitions the ladder.
-4. Open a PR `staging → main` and squash-merge it. The squash message should be a conventional commit (`docs(release): vX.Y.Z` or similar) so the changelog stays clean.
-5. Tag `vX.Y.Z[-pre]` on `main`; push the tag.
-6. Verify the GitHub Release fires and Zenodo mints the version DOI.
-7. Fill the concept DOI into `CITATION.cff` `identifiers:` and the badge in `README.md` (once, after the first release).
+1. Bump `version` in `package.json` to match the new tag. The compiler reads it, so it also becomes the `datapackage.json` version of the published dump.
+2. Set `version` and `date-released` in `CITATION.cff` to the same tag and its release date. Without them the file cannot say which release it describes.
+3. `npx git-cliff --tag vX.Y.Z -o CHANGELOG.md` to regenerate `CHANGELOG.md`. Pass `--tag` explicitly: the tag does not exist yet at this point, and bare `npm run changelog` would file the commits under `## [Unreleased]`.
+4. Update spec page frontmatter `maturity:` if the release transitions the ladder.
+5. Dispatch the **Pages** workflow on `staging`. `main`'s ruleset requires a successful `github-pages` deployment for the exact SHA being merged, so the release PR stays blocked until the branch tip has one.
+6. Open a PR `staging → main` and squash-merge it. The squash message should be a conventional commit (`docs(release): vX.Y.Z` or similar) so the changelog stays clean.
+7. Tag `vX.Y.Z[-pre]` on `main`; push the tag.
+8. Verify the GitHub Release fires and Zenodo mints the version DOI.
+9. Fill the concept DOI into `CITATION.cff` `identifiers:` and the badge in `README.md` (once, after the first release).
**Registry** ([`textrefs/registry`](https://github.com/textrefs/registry)):
diff --git a/src/content/docs/standard/versioning.md b/src/content/docs/standard/versioning.md
index ffa5f83..622466d 100644
--- a/src/content/docs/standard/versioning.md
+++ b/src/content/docs/standard/versioning.md
@@ -67,8 +67,9 @@ Each `datapackage.json` MUST include:
- `name`: `textrefs-registry`.
- `version`: SemVer package version.
- `licenses`: SPDX identifier `CC0-1.0` for registry data.
-- `resources`: one resource per JSONL file.
-- `schema`: field descriptors for each resource.
+- `resources`: one resource per JSONL file, each carrying `path`, `format`, `mediatype`, `encoding`, `bytes`, and a `sha256:` `hash` of the resource body.
+
+Resources SHOULD additionally carry a Frictionless `schema` with field descriptors. Exports through `v0.1.0` omit it: record shapes are normative in the [specification](/standard/specification/) and enforced by the compiler, and restating them as Frictionless descriptors is [tracked separately](https://github.com/textrefs/textrefs.org/issues/74). Validate against the published shapes, not against the data package alone.
## Per-record versioning
From fa78eb9474242805289c11a42aa71b6a4e70553c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
Date: Wed, 12 Aug 2026 23:37:14 +0200
Subject: [PATCH 38/45] ci: bump actions/setup-node to v7 (#32) (#77)
---
.github/workflows/pages.yml | 2 +-
.github/workflows/release.yml | 2 +-
.github/workflows/validate.yml | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
index 94f37df..bc2ec8a 100644
--- a/.github/workflows/pages.yml
+++ b/.github/workflows/pages.yml
@@ -25,7 +25,7 @@ jobs:
with:
submodules: recursive
- - uses: actions/setup-node@v6
+ - uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index e3d461d..bf481aa 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -17,7 +17,7 @@ jobs:
with:
submodules: recursive
- - uses: actions/setup-node@v6
+ - uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index edf2db6..c9256d0 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -25,7 +25,7 @@ jobs:
echo "::error::data/ pointer $(git rev-parse HEAD) is not an ancestor of registry/main"
exit 1
fi
- - uses: actions/setup-node@v6
+ - uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
From 37faf3761f067806ebf0068e43d17be7d2061d79 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
Date: Wed, 12 Aug 2026 23:43:03 +0200
Subject: [PATCH 39/45] ci(deps): point Dependabot version updates at staging
(#78)
---
.github/dependabot.yml | 30 ++++++++++++++++++++++++++++++
1 file changed, 30 insertions(+)
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index f94564f..cf2d947 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,10 +1,40 @@
+# Version updates target `staging`, not the default branch.
+#
+# `main` only moves at release time, by squash-merging `staging`. A bump landed
+# on `main` alone is therefore reverted by the next release: the squash applies
+# staging's tree against a merge base that predates the bump. Everything that
+# should survive a release has to reach `main` through `staging`.
+#
+# Note that `target-branch` governs version updates only — Dependabot security
+# updates are always raised against the default branch, so advisory-driven PRs
+# will still appear against `main` and still need folding into `staging` by
+# hand.
version: 2
updates:
- package-ecosystem: github-actions
directory: /
+ target-branch: staging
schedule:
interval: weekly
groups:
github-actions:
patterns:
- '*'
+
+ - package-ecosystem: npm
+ directory: /
+ target-branch: staging
+ schedule:
+ interval: weekly
+ groups:
+ # Astro, Starlight and their plugins move in lockstep and are upgraded
+ # together or not at all; a lone bump here usually fails to resolve.
+ astro:
+ patterns:
+ - 'astro'
+ - '@astrojs/*'
+ - 'starlight*'
+ # First match wins, so this picks up everything else in one PR.
+ npm:
+ patterns:
+ - '*'
From 3f6670e70fd302f22ba412bf0eab80a709226146 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
Date: Thu, 13 Aug 2026 15:59:12 +0200
Subject: [PATCH 40/45] docs: resolve the consistency audit before v0.1.0 (#79)
(#80)
Resolves 40 of the 41 findings in the documentation consistency audit (#79) and the related-systems rework (#81), ahead of the v0.1.0 tag.
- fix(standard): spec worked example, validation rules, and reference drift
- fix(api): align OpenAPI with the contract and validate BCP 47 tags
- docs(community): resync the mirrored contributing and roadmap files
- docs(standard): fix json-ld, versioning, and index drift
- docs(get-started): correct every example against the compiled registry
- docs(get-started): plain-language pass
- docs(get-started): group related standards by the layer they address (#81)
- docs(standard): resolve review findings on the license and language rules
F36 is excluded deliberately; it belongs with the statutes batch.
---
.../ISSUE_TEMPLATE/conformance_failure.yml | 4 +-
AGENTS.md | 9 +-
CONTRIBUTING.md | 2 +-
README.md | 9 +-
ROADMAP.md | 2 +-
api/openapi.yaml | 38 +++++-
scripts/compile.test.ts | 68 ++++++++++
scripts/source-schema.ts | 9 +-
src/content/docs/community/contributing.md | 34 +++--
src/content/docs/community/roadmap.md | 4 +-
src/content/docs/get-started/authoring.md | 125 +++++++++++-------
src/content/docs/get-started/how-it-works.md | 63 ++++++---
src/content/docs/get-started/index.md | 10 +-
.../mappings-and-resolver-targets.md | 54 ++++----
.../docs/get-started/related-systems.md | 118 +++++++++++++----
src/content/docs/get-started/url-layout.md | 28 ++--
src/content/docs/get-started/use-cases.md | 20 +--
src/content/docs/standard/index.md | 10 +-
src/content/docs/standard/json-ld.md | 18 +--
src/content/docs/standard/specification.md | 21 +--
src/content/docs/standard/versioning.md | 8 +-
standard/schema/canonical-reference.ts | 4 +-
standard/schema/common.ts | 14 ++
standard/schema/work.ts | 4 +-
24 files changed, 465 insertions(+), 211 deletions(-)
diff --git a/.github/ISSUE_TEMPLATE/conformance_failure.yml b/.github/ISSUE_TEMPLATE/conformance_failure.yml
index f1a74ff..a97c8e8 100644
--- a/.github/ISSUE_TEMPLATE/conformance_failure.yml
+++ b/.github/ISSUE_TEMPLATE/conformance_failure.yml
@@ -23,9 +23,9 @@ body:
id: failing-cases
attributes:
label: Failing test cases
- description: List the conformance test IDs that fail and what your implementation produces instead.
+ description: Describe the failing case and what your implementation produces instead. Include conformance test IDs if the conformance suite is available.
validations:
- required: true
+ required: false
- type: textarea
id: analysis
attributes:
diff --git a/AGENTS.md b/AGENTS.md
index b1131c6..07fea0b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -9,8 +9,8 @@ Astro + Starlight site for textrefs.org. See [`README.md`](./README.md) for the
- `npm run build:fast` — site build against a tiny fixture registry, without compiling full data
- `npm run format` — Prettier (with `prettier-plugin-astro`)
- `npm run check` — `astro check`
-- `npm run verify:fast` — fast local verification: format:check + fixture-backed check + fixture-backed build
-- `npm run verify` — format:check + check + build (the CI gate)
+- `npm run verify:fast` — fast local verification: format:check + fixture-backed check + tests + fixture-backed build
+- `npm run verify` — format:check + check + tests + build (the CI gate)
- `npm run validate:data` / `compile:data` / `build:data` — registry-data gates; see [`data/AGENTS.md`](./data/AGENTS.md)
Use `npm run verify:fast` by default for local validation and agent work. Only use the full `npm run verify` and/or `npm run validate:data` when changes affect registry data, release artifacts, production build behavior, or CI parity.
@@ -29,7 +29,8 @@ Husky installs on `npm install`: Prettier runs on staged files, `commitlint` enf
- `src/styles/brand.css` — brand tokens
- `public/{logo.svg,favicon.svg,fonts/}` — brand assets
- `data/` — git submodule pointing at [`textrefs/registry`](https://github.com/textrefs/registry); hand-authored YAML lives there. See [`data/AGENTS.md`](./data/AGENTS.md) for authoring rules and [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the submodule workflow.
-- `standard/`, `api/` — scaffolds reserved for future repo splits
+- `standard/` — draft standard workspace: canonical Zod schemas (`standard/schema/`)
+- `api/` — the OpenAPI contract (`api/openapi.yaml`)
## Conventions
@@ -47,7 +48,7 @@ Husky installs on `npm install`: Prettier runs on staged files, `commitlint` enf
## Mirrored community files
-`CODE_OF_CONDUCT.md`, `CONTRIBUTING.md`, and `SECURITY.md` live at the repo root (for GitHub's Community profile) and are mirrored at `src/content/docs/community/` with Starlight frontmatter. Edit both in the same commit.
+`CODE_OF_CONDUCT.md`, `CONTRIBUTING.md`, and `SECURITY.md` live at the repo root for GitHub's Community profile; `ROADMAP.md` lives there as the canonical roadmap text. All four are mirrored at `src/content/docs/community/` with Starlight frontmatter. Edit both halves of a pair in the same commit.
## Pointers
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 30e92d9..3d5dfd1 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -66,7 +66,7 @@ Registry data lives in [`textrefs/registry`](https://github.com/textrefs/registr
Before pushing routine documentation, styling, or route work, run the fast local gate:
```sh
-npm run verify:fast # Prettier check + fixture-backed astro check + fixture-backed build
+npm run verify:fast # Prettier check + fixture-backed astro check + tests + fixture-backed build
```
Run the full `npm run verify` before PRs that touch registry data, release output, production build behaviour, or CI behaviour. Run `npm run validate:data` as well for registry-data and standard PRs.
diff --git a/README.md b/README.md
index 823d7c5..bce8fb7 100644
--- a/README.md
+++ b/README.md
@@ -49,13 +49,16 @@ TextRefs is a non-profit infrastructure project that builds, maintains, and publ
├── src/
│ ├── components/ # Starlight component overrides (Footer)
│ ├── content/docs/ # site content (English at root, German under de/)
+│ ├── layouts/ # shared page layouts (e.g. canonical record pages)
+│ ├── lib/ # registry loading, citation, and banner helpers
+│ ├── pages/ # id/, reg/, cite/ routes (see AGENTS.md)
│ ├── styles/brand.css # brand tokens (see public/BRAND notes)
│ └── content.config.ts
├── data/ # git submodule → textrefs/registry (hand-authored YAML)
├── scripts/ # data compile + validate pipeline
-├── standard/, api/ # scaffolds reserved for future repo splits
+├── standard/ # draft standard workspace: Zod schemas, JSON-LD context
+├── api/ # OpenAPI contract (api/openapi.yaml)
├── decisions/ # Architecture Decision Records (MADR)
-├── docs-internal/ # maintainer-only notes, not published
├── astro.config.mjs # Astro + Starlight config (i18n, sidebar)
├── cliff.toml # git-cliff config for CHANGELOG generation
├── commitlint.config.js # conventional-commit enforcement
@@ -79,7 +82,7 @@ Configuration lives in `.env`; use [`.env.example`](./.env.example) as the start
| `npm run validate:data` | Validate every compiled record against the canonical Zod schemas |
| `npm run build:data` | `compile:data` then `validate:data` — the contributor data pipeline |
| `npm run verify:fast` | Fast local check using fixture registry data |
-| `npm run verify` | Prettier + `astro check` + production build — the CI gate |
+| `npm run verify` | Prettier + `astro check` + tests + production build — the CI gate |
| `npm run changelog` | Regenerate `CHANGELOG.md` from git history (git-cliff) |
Contributors edit the YAML under [`data/works/`](https://github.com/textrefs/registry/tree/main/works) and [`data/systems/`](https://github.com/textrefs/registry/tree/main/systems); the directory is a git submodule pointing at [`textrefs/registry`](https://github.com/textrefs/registry). Run `git submodule update --init --recursive` after cloning. The compiler expands the pinned submodule into the flat registry dump (works, systems, refs, mappings) under `dist/dump/`. See [`docs/get-started/authoring`](https://textrefs.org/get-started/authoring/) for the format. For documentation, styling, and route work, use `npm run verify:fast` locally; run the full `npm run verify` before PRs that touch registry data, release output, production build behaviour, or CI behaviour.
diff --git a/ROADMAP.md b/ROADMAP.md
index 08fb26d..8c70240 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -18,7 +18,7 @@ Status legend: done · in progress · planned · blocked.
- done: Get-started section (welcome, use cases, related identifier systems, mappings, and authoring guidance).
- done: Standard draft workspace with Zod schemas and JSON-LD context.
- done: Seed registry data and static registry browsing pages.
-- in progress: Replace remaining community placeholder pages with contributor-facing guidance.
+- done: Replace remaining community placeholder pages with contributor-facing guidance.
- done: Serve canonical record pages under `/id/work/{key}/`, `/id/system/{key}/`, `/id/ref/{uuid}/`, `/id/mapping/{uuid}/`, each with a `.json` JSON-LD sibling advertised via ``.
- done: Decide how the `data/` repo split is packaged — extracted to [`textrefs/registry`](https://github.com/textrefs/registry) and included here as a git submodule.
diff --git a/api/openapi.yaml b/api/openapi.yaml
index 76bd354..816b02a 100644
--- a/api/openapi.yaml
+++ b/api/openapi.yaml
@@ -10,7 +10,7 @@ info:
- `/id/{type}/{key}.json` — JSON-LD for machines.
There is **no** `Accept`-header content negotiation. Clients either
- follow the ``
+ follow the ``
advertised in the HTML head, or simply append `.json` to the canonical
URL. JSON bodies carry the JSON-LD `@context` at
[`/contexts/v1.jsonld`](https://textrefs.org/contexts/v1.jsonld); the
@@ -196,6 +196,24 @@ components:
description:
Successor IRI. Only present when status is deprecated, withdrawn,
or blocked.
+ Creator:
+ description:
+ Discriminated on `kind`. A `person` entry names a family name (and
+ optional given name); mononyms such as Plato use `family` alone. A
+ `literal` entry is the escape hatch for pseudonymous, collective, or
+ institutional authorship.
+ oneOf:
+ - type: object
+ required: [kind, family]
+ properties:
+ kind: { type: string, enum: [person] }
+ family: { type: string, minLength: 1 }
+ given: { type: string, minLength: 1 }
+ - type: object
+ required: [kind, name]
+ properties:
+ kind: { type: string, enum: [literal] }
+ name: { type: string, minLength: 1 }
Work:
allOf:
- $ref: '#/components/schemas/AdminMetadata'
@@ -217,18 +235,21 @@ components:
Governs the bare /cite/{work_key}/{locator} alias and default
presentation only; it is identity-neutral and never affects how
a fully qualified reference validates or resolves.
+ creators:
+ type: array
+ items: { $ref: '#/components/schemas/Creator' }
alternateOf:
type: array
items: { $ref: '#/components/schemas/Iri' }
description:
- Compiler-derived from the work's non-tombstoned alternateOf
- MappingAssertions. Read-only, never authored.
+ Compiler-derived from the work's alternateOf MappingAssertions
+ that are not withdrawn or blocked. Read-only, never authored.
isReferencedBy:
type: array
items: { $ref: '#/components/schemas/Iri' }
- description:
- Compiler-derived from the work's non-tombstoned isReferencedBy
- MappingAssertions. Read-only, never authored.
+ description: Compiler-derived from the work's isReferencedBy
+ MappingAssertions that are not withdrawn or blocked.
+ Read-only, never authored.
CitationSystem:
allOf:
- $ref: '#/components/schemas/AdminMetadata'
@@ -248,7 +269,10 @@ components:
required: [url, access]
properties:
url: { $ref: '#/components/schemas/Iri' }
- language: { type: string, minLength: 2 }
+ language:
+ type: string
+ minLength: 2
+ pattern: '^(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}){0,3}|[A-Za-z]{4,8})(?:-[A-Za-z]{4})?(?:-(?:[A-Za-z]{2}|\d{3}))?(?:-(?:[\dA-Za-z]{5,8}|\d[\dA-Za-z]{3}))*(?:-[\dA-WY-Za-wy-z](?:-[\dA-Za-z]{2,8})+)*(?:-[Xx](?:-[\dA-Za-z]{1,8})+)?$|^[Xx](?:-[\dA-Za-z]{1,8})+$'
edition: { type: string }
provider: { type: string }
access:
diff --git a/scripts/compile.test.ts b/scripts/compile.test.ts
index b6a70f8..dc453d4 100644
--- a/scripts/compile.test.ts
+++ b/scripts/compile.test.ts
@@ -11,6 +11,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { v5 as uuidv5 } from 'uuid';
import { compileRegistry, type CompiledRegistry } from './compile.js';
+import { LanguageTag } from '../standard/schema/common.js';
type RegistryFiles = {
systems: Record;
@@ -568,3 +569,70 @@ test('url_by with more than one selector variable is rejected at parse time', (t
);
assert.match(logged.join('\n'), /url_by takes exactly one selector variable/);
});
+
+// --- Resolver language tags (BCP 47) -----------------------------------------
+
+test('a malformed language tag is rejected at parse time', (t) => {
+ const message = expectCompileError(
+ t,
+ workWithResolver(` - url: 'https://example.org/{book}/{chapter}'
+ language: 'e n'`),
+ );
+ assert.match(message, /test\.work/);
+});
+
+test('a well-formed subtagged language tag is accepted', () => {
+ const reg = compileFixture(
+ workWithResolver(` - url: 'https://example.org/{book}/{chapter}'
+ language: grc-Grek`),
+ );
+ assert.equal(
+ reg.references.find((r) => r.locator === 'Gen.1')?.resolver_targets[0]
+ ?.language,
+ 'grc-Grek',
+ );
+});
+
+// RFC 5646 keeps grandfathered tags outside the langtag production it defines,
+// so `LanguageTag` accepts or rejects them by whether they happen to parse as a
+// langtag. This locks that split, which the comment on `LanguageTag` describes.
+test('regular grandfathered tags parse and irregular ones do not', () => {
+ const regular = [
+ 'art-lojban',
+ 'cel-gaulish',
+ 'no-bok',
+ 'no-nyn',
+ 'zh-guoyu',
+ 'zh-hakka',
+ 'zh-min',
+ 'zh-min-nan',
+ 'zh-xiang',
+ ];
+ const irregular = [
+ 'en-GB-oed',
+ 'i-ami',
+ 'i-bnn',
+ 'i-default',
+ 'i-enochian',
+ 'i-hak',
+ 'i-klingon',
+ 'i-lux',
+ 'i-mingo',
+ 'i-navajo',
+ 'i-pwn',
+ 'i-tao',
+ 'i-tay',
+ 'i-tsu',
+ 'sgn-BE-FR',
+ 'sgn-BE-NL',
+ 'sgn-CH-DE',
+ ];
+ assert.deepEqual(
+ regular.filter((tag) => !LanguageTag.safeParse(tag).success),
+ [],
+ );
+ assert.deepEqual(
+ irregular.filter((tag) => LanguageTag.safeParse(tag).success),
+ [],
+ );
+});
diff --git a/scripts/source-schema.ts b/scripts/source-schema.ts
index c96ea32..717abd3 100644
--- a/scripts/source-schema.ts
+++ b/scripts/source-schema.ts
@@ -6,7 +6,12 @@
// at all. Objects are strict: an unknown key is an authoring error.
import { createRequire } from 'node:module';
import { z } from 'zod';
-import { FlatKey, IsoDate, Status } from '../standard/schema/common.js';
+import {
+ FlatKey,
+ IsoDate,
+ LanguageTag,
+ Status,
+} from '../standard/schema/common.js';
const require = createRequire(import.meta.url);
const spdxLicenseIds: string[] = require('spdx-license-ids');
@@ -39,7 +44,7 @@ export const ResolverEntrySource = z
.optional(),
provider: z.string().min(1).optional(),
edition: z.string().min(1).optional(),
- language: z.string().min(2).optional(),
+ language: LanguageTag.optional(),
access: z.enum(['open', 'paywalled', 'restricted', 'unknown']).optional(),
// Checked here rather than at emit time: the compiler maps an id to its
// canonical SPDX IRI, and anything it cannot map would otherwise drop out
diff --git a/src/content/docs/community/contributing.md b/src/content/docs/community/contributing.md
index cf2ce1e..797f074 100644
--- a/src/content/docs/community/contributing.md
+++ b/src/content/docs/community/contributing.md
@@ -71,7 +71,7 @@ Registry data lives in [`textrefs/registry`](https://github.com/textrefs/registr
Before pushing routine documentation, styling, or route work, run the fast local gate:
```sh
-npm run verify:fast # Prettier check + fixture-backed astro check + fixture-backed build
+npm run verify:fast # Prettier check + fixture-backed astro check + tests + fixture-backed build
```
Run the full `npm run verify` before PRs that touch registry data, release output, production build behaviour, or CI behaviour. Run `npm run validate:data` as well for registry-data and standard PRs.
@@ -97,13 +97,25 @@ The commit-msg hook (commitlint) rejects non-conforming messages, so a plain `gi
The changelog is generated from this history via `npm run changelog` (git-cliff).
+## Branching model
+
+The production site (`textrefs.org`) is built and deployed from `main`. To keep `main`'s history low-noise while still allowing many small content edits, day-to-day docs/blog/copy work batches on a long-lived `staging` branch and is squash-merged into `main` to publish.
+
+- `main` — production source. Pushes here auto-deploy via `.github/workflows/pages.yml`. Release tags (`vX.Y.Z`) are cut from `main`; the registry's `vYYYY.MM.N` tags are cut in [`textrefs/registry`](https://github.com/textrefs/registry).
+- `staging` — long-lived batching branch for docs, blog posts, copy, registry-pointer bumps, and other content edits. Does **not** auto-deploy. Edits accumulate here as many small commits.
+- To publish: open a PR `staging → main` and squash-merge. The squash-commit lands on `main` as one conventional commit (so `git-cliff` stays clean) and triggers the production deploy.
+- Manual preview / ad-hoc deploy: from the GitHub Actions UI, run the **Pages** workflow via `workflow_dispatch` and pick `staging` (or any branch) as the ref. This deploys that ref to production until the next push to `main`. There is no separate preview URL — GitHub Pages serves a single site per repo, so manual staging deploys temporarily replace production. Use sparingly.
+- Infrastructure changes (CI, release workflow, build tooling, deploy config) target `main` directly so they are not gated on the next staging-to-main snapshot.
+
+Squash merging is the only enabled merge style on the canonical repo, so `staging`'s noisy history is collapsed into a single conventional-commit message on `main` and `git-cliff` still produces a clean `CHANGELOG.md`.
+
## Submitting a pull request
-1. Branch from `main`.
+1. Branch from `staging` for content/docs/blog; branch from `main` for infra, CI, or release-workflow changes.
2. Keep PRs focused — one logical change per PR.
3. Link related issues in the PR description.
4. Include local verification results: `npm run verify:fast` for routine work, or `npm run verify` plus `npm run validate:data` for registry-data, standard, release, production-build, or CI changes.
-5. Open the PR against `main`. GitHub requests `@textrefs/maintainers` by default via `.github/CODEOWNERS`; maintainers may add technical or expert reviewers based on the track.
+5. Open the PR against the branch you started from (`staging` or `main`). GitHub requests `@textrefs/maintainers` by default via `.github/CODEOWNERS`; maintainers may add technical or expert reviewers based on the track.
## Project layout
@@ -115,13 +127,15 @@ Two release trains. The Zenodo–GitHub webhook MUST be enabled once per reposit
**Standard + site** (this repo):
-1. Bump `version` in `package.json` to match the new tag.
-2. `npm run changelog` to regenerate `CHANGELOG.md`.
-3. Update spec page frontmatter `maturity:` if the release transitions the ladder.
-4. Commit, open PR, merge to `main`.
-5. Tag `vX.Y.Z[-pre]` on `main`; push the tag.
-6. Verify the GitHub Release fires and Zenodo mints the version DOI.
-7. Fill the concept DOI into `CITATION.cff` `identifiers:` and the badge in `README.md` (once, after the first release).
+1. Bump `version` in `package.json` to match the new tag. The compiler reads it, so it also becomes the `datapackage.json` version of the published dump.
+2. Set `version` and `date-released` in `CITATION.cff` to the same tag and its release date. Without them the file cannot say which release it describes.
+3. `npx git-cliff --tag vX.Y.Z -o CHANGELOG.md` to regenerate `CHANGELOG.md`. Pass `--tag` explicitly: the tag does not exist yet at this point, and bare `npm run changelog` would file the commits under `## [Unreleased]`.
+4. Update spec page frontmatter `maturity:` if the release transitions the ladder.
+5. Dispatch the **Pages** workflow on `staging`. `main`'s ruleset requires a successful `github-pages` deployment for the exact SHA being merged, so the release PR stays blocked until the branch tip has one.
+6. Open a PR `staging → main` and squash-merge it. The squash message should be a conventional commit (`docs(release): vX.Y.Z` or similar) so the changelog stays clean.
+7. Tag `vX.Y.Z[-pre]` on `main`; push the tag.
+8. Verify the GitHub Release fires and Zenodo mints the version DOI.
+9. Fill the concept DOI into `CITATION.cff` `identifiers:` and the badge in `README.md` (once, after the first release).
**Registry** ([`textrefs/registry`](https://github.com/textrefs/registry)):
diff --git a/src/content/docs/community/roadmap.md b/src/content/docs/community/roadmap.md
index 4374a21..a2365a1 100644
--- a/src/content/docs/community/roadmap.md
+++ b/src/content/docs/community/roadmap.md
@@ -5,7 +5,7 @@ sidebar:
order: 7
---
-This is the public roadmap. The same content is mirrored on the docs site at [textrefs.org/community/roadmap/](/community/roadmap/).
+This is the public roadmap. The canonical text is [`ROADMAP.md`](https://github.com/textrefs/textrefs.org/blob/main/ROADMAP.md) in the repo; this page mirrors it.
## Status
@@ -23,7 +23,7 @@ Status legend: done · in progress · planned · blocked.
- done: Get-started section (welcome, use cases, related identifier systems, mappings, and authoring guidance).
- done: Standard draft workspace with Zod schemas and JSON-LD context.
- done: Seed registry data and static registry browsing pages.
-- in progress: Replace remaining community placeholder pages with contributor-facing guidance.
+- done: Replace remaining community placeholder pages with contributor-facing guidance.
- done: Serve canonical record pages under `/id/work/{key}/`, `/id/system/{key}/`, `/id/ref/{uuid}/`, `/id/mapping/{uuid}/`, each with a `.json` JSON-LD sibling advertised via ``.
- done: Decide how the `data/` repo split is packaged — extracted to [`textrefs/registry`](https://github.com/textrefs/registry) and included here as a git submodule.
diff --git a/src/content/docs/get-started/authoring.md b/src/content/docs/get-started/authoring.md
index b0c62b4..d41061d 100644
--- a/src/content/docs/get-started/authoring.md
+++ b/src/content/docs/get-started/authoring.md
@@ -5,7 +5,9 @@ sidebar:
order: 6
---
-Hand-authored YAML lives in the separate [`textrefs/registry`](https://github.com/textrefs/registry) repository, mounted in this repo as a git submodule at `data/` (run `git submodule update --init --recursive` after cloning; see [`CONTRIBUTING.md`](https://github.com/textrefs/textrefs.org/blob/main/CONTRIBUTING.md) for the full workflow). Contributors edit `data/works/{work_key}.yaml` and `data/systems/{system_key}.yaml` there. The compiled registry — Works, CitationSystems, CanonicalReferences, MappingAssertions — is produced by `npm run build:data` from the pinned submodule pointer and written as JSONL resources plus `datapackage.json` under `dist/dump/`. Published dumps are attached to TextRefs Standard/site releases and long-term archived in the [TextRefs Zenodo community](https://zenodo.org/communities/textrefs/) with citable DOIs.
+Hand-authored YAML lives in the separate [`textrefs/registry`](https://github.com/textrefs/registry) repository. This repo mounts it as a git submodule at `data/`. After cloning, run `git submodule update --init --recursive`. See [`CONTRIBUTING.md`](https://github.com/textrefs/textrefs.org/blob/main/CONTRIBUTING.md) for the full workflow. Contributors edit `data/works/{work_key}.yaml` and `data/systems/{system_key}.yaml` there.
+
+`npm run build:data` produces the compiled registry from the pinned submodule pointer. The compiled registry holds Works, CitationSystems, CanonicalReferences, and MappingAssertions. The command writes them as JSONL resources plus `datapackage.json` under `dist/dump/`. Published dumps are attached to TextRefs Standard releases and site releases. They are also archived long-term in the [TextRefs Zenodo community](https://zenodo.org/communities/textrefs/) with citable DOIs.
This page documents the YAML format.
@@ -17,11 +19,19 @@ data/
└── systems/{system_key}.yaml # one file per CitationSystem
```
-A `Work` source file declares the work itself, references the citation system it uses as its **preferred** citation system (optionally alongside fallback systems, see [Additional citation systems](#additional-citation-systems-and-reference_status)), lists references, optionally lists resolver templates, and optionally lists work-level mappings. A `CitationSystem` source file declares the locator regex and examples.
+A `Work` source file declares:
+
+- the work itself;
+- its preferred citation system, and any fallback systems (see [Additional citation systems](#additional-citation-systems-and-reference_status));
+- its references;
+- its resolver templates, which are optional;
+- its work-level mappings, which are also optional.
+
+A `CitationSystem` source file declares the locator regex and a prose description of the locator format.
## A worked example
-The Dhammapada has 423 verses across 26 chapters and is hosted on four different reading platforms with four different URL patterns. The whole work, with chapter 1 fully wired up, is roughly 60 lines of YAML:
+The Dhammapada has 423 verses across 26 chapters and is hosted on two different reading platforms with two different URL patterns. The whole work, with chapter 1 fully wired up, is roughly 60 lines of YAML:
```yaml
# data/works/dhammapada.yaml
@@ -36,7 +46,7 @@ citation_system: dhammapada-chapter-verse # the work's PREFERRED citation system
mappings:
- relation: alternateOf
- identifier: 'https://www.wikidata.org/entity/Q220114'
+ identifier: 'https://www.wikidata.org/entity/Q748878'
conforms_to: 'https://www.wikidata.org/'
source: manual-curation
status: draft
@@ -66,9 +76,9 @@ references:
# …
```
-Each reference gets one entry per resolver. The compiler derives `{verseGlobal}` from the citation system's `chapter_sizes` (see [How URL templates work](#how-url-templates-work)), and looks up `url_by.chapter[N]` for providers whose URL structure isn't templatable.
+Each reference gets one entry per resolver. The compiler derives `{verseGlobal}` from the citation system's `chapter_sizes`. See [How URL templates work](#how-url-templates-work). For providers whose URL structure is not templatable, the compiler looks up `url_by.chapter[N]` instead.
-Every resolver must actually address the cited passage. A URL with no locator variable in it resolves every reference in the work to the same landing page, which is worse than having no resolver at all — the registry says "here is 1.1" and hands the reader a book.
+Every resolver must address the cited passage. A URL with no locator variable resolves every reference in the work to the same landing page. That is worse than having no resolver at all: the registry says "here is 1.1" and hands the reader a book.
### Optional: `creators`
@@ -90,53 +100,59 @@ work:
name: '[Pseudo-]Aristotle' # institutions, collective, or pseudonymous attribution
```
-Omit `creators` entirely for anonymous or non-authored works (e.g. the Dhammapada, the Bible). For attributed-but-disputed works, record the traditional attribution for citation rendering and document uncertainty through mappings or review notes. The field is purely optional; nothing in the registry depends on it.
+Omit `creators` entirely for anonymous or non-authored works, for example the Dhammapada or the Bible. For attributed-but-disputed works, record the traditional attribution for citation rendering. Document the uncertainty through mappings or review notes instead. The field is optional. Nothing in the registry depends on it.
## Naming and identity
-Get `work.key`, `work.preferred_label`, and `work.creators` right on the first commit — renaming a key after publication is a tombstone event that mints new reference IRIs.
+Get `work.key`, `work.preferred_label`, and `work.creators` right on the first commit. Renaming a key after publication is a tombstone event. It mints new reference IRIs.
### `key`
-Shape: `{author-slug}.{work-slug}` for attributed works; bare `{work-slug}` for anonymous, collective, or canonical corpora.
+The key has two shapes. Attributed works use `{author-slug}.{work-slug}`. Anonymous, collective, or canonical corpora use a bare `{work-slug}`.
-- `author-slug` — lowercased family name (or single mononym for antiquity); ASCII-folded; `-` for spaces; no initials. E.g. `homer`, `plato`, `aristotle`, `wittgenstein`, `confucius`, `laozi`, `murasaki-shikibu`.
-- `work-slug` — the short form readers actually use: `iliad`, `republic`, `tractatus`, `analects`, `daodejing`. Avoid cryptic initialisms (`eth-nic`) and avoid full Latin titles unless that _is_ the short form.
+- `author-slug`: the lowercased family name, or a single mononym for antiquity. ASCII-fold it. Use `-` for spaces. Do not use initials. For example: `homer`, `plato`, `aristotle`, `wittgenstein`, `confucius`, `laozi`, `murasaki-shikibu`.
+- `work-slug`: the short form readers use, for example `iliad`, `republic`, `tractatus`, `analects`, `daodejing`. Avoid cryptic initialisms such as `eth-nic`. Avoid full Latin titles unless the Latin title is the short form.
- Bare slug for unattributed corpora: `tanakh`, `dhammapada`, `new-testament`, `quran`.
-- Multiple works per author with the same short title: disambiguate inside the work-slug, not by promoting the author. E.g. `aristotle.nicomachean-ethics`, `aristotle.eudemian-ethics`.
+- If an author has multiple works with the same short title, disambiguate inside the work-slug rather than by promoting the author. For example: `aristotle.nicomachean-ethics`, `aristotle.eudemian-ethics`.
### `preferred_label`
-The display title. No parenthetical disambiguator — author goes in `creators`, edition (SBLGNT, OCT, …) goes on the resolver target, alt-names belong in a future `alt_labels` field.
+The display title. Do not add a parenthetical disambiguator:
+
+- the author goes in `creators`;
+- the edition, such as SBLGNT or OCT, goes on the resolver target;
+- alt-names belong in a future `alt_labels` field.
+
+Use these forms:
-- Attributed: just the title — `Iliad`, `Republic`, `Tractatus Logico-Philosophicus`.
-- Anonymous / collective: the conventional English name — `Tanakh`, `Dhammapada`, `New Testament`.
+- Attributed works: just the title, for example `Iliad`, `Republic`, `Tractatus Logico-Philosophicus`.
+- Anonymous or collective works: the conventional English name, for example `Tanakh`, `Dhammapada`, `New Testament`.
### `creators`
-Follow CSL-JSON conventions so citeproc-js / Zotero render correctly.
+Follow CSL-JSON conventions so citeproc-js and Zotero render correctly.
-- Standard names: `kind: person` with `family` and `given`. E.g. `{ kind: person, family: Wittgenstein, given: Ludwig }`.
-- Mononyms (Homer, Plato, Confucius, Laozi, Murasaki Shikibu, …): `kind: person` with `family` only and no `given`. CSL convention for single-name authors; matches Chicago's "Homer, _Iliad_ 1.1." output.
-- Anonymous / collective: **omit `creators` entirely**. Don't write a literal "Anonymous" — absence is the correct CSL signal.
-- Reserve `kind: literal` for names that genuinely should not decompose: corporate/institutional authors ("World Health Organization") or pseudonymous attribution strings ("[Pseudo-]Aristotle").
-- Attributed-but-disputed (e.g. Laozi for _Daodejing_): record the traditional attribution as `kind: person, family: Laozi`; do not encode the dispute in the name string. TextRefs has no relation for attribution uncertainty in this version.
+- Standard names: `kind: person` with `family` and `given`. For example: `{ kind: person, family: Wittgenstein, given: Ludwig }`.
+- Mononyms, for example Homer, Plato, Confucius, Laozi, or Murasaki Shikibu: use `kind: person` with `family` only and no `given`. This is the CSL convention for single-name authors. It matches Chicago's output "Homer, _Iliad_ 1.1."
+- Anonymous or collective: **omit `creators` entirely**. Do not write a literal "Anonymous". The absence of the field is the correct CSL signal.
+- Reserve `kind: literal` for names that should not decompose: corporate or institutional authors, for example "World Health Organization", or pseudonymous attribution strings, for example "[Pseudo-]Aristotle".
+- Attributed-but-disputed works, for example Laozi for _Daodejing_: record the traditional attribution as `kind: person, family: Laozi`. Do not encode the dispute in the name string. TextRefs has no relation for attribution uncertainty in this version.
## How URL templates work
-The compiler treats every resolver `url` as an [RFC 6570](https://www.rfc-editor.org/rfc/rfc6570) Level 1 template. Variables are drawn from two sources:
+The compiler treats every resolver `url` as an [RFC 6570](https://www.rfc-editor.org/rfc/rfc6570) Level 1 template. Variables are drawn from five sources:
1. **Named capture groups** in the citation system's `locator_regex`. For example, a regex like `^(?\d+)\.(?\d+)$` exposes `{chapter}` and `{verse}` to every template.
2. **Zero-padded variants** of any numeric capture, generated automatically: `{chapter02}`, `{chapter03}`, `{chapter04}`, `{verse02}`, `{verse03}`. Use the padding width that matches the target site's URL.
-3. **Roman-numeral variants** of any numeric capture in 1..3999, generated automatically: `{chapterRoman}` produces `I`, `VIII`, `XXVI`. Useful for sites that anchor sections by Roman chapter (e.g. Wikisource's `#I:8` Dhammapada verses).
-4. **Cumulative `{verseGlobal}`** — for systems whose locators have numeric `chapter` and `verse` groups _and_ declare `chapter_sizes:` (see below), the compiler exposes a global 1..N verse counter. Useful for single-page resolvers (e.g. SuttaCentral's `/dhp8`, `/dhp102`) whose URLs use one running index across all chapters.
-5. **Provider-specific spellings** declared per resolver with `vars:` — see below.
+3. **Roman-numeral variants** of any numeric capture in 1..3999, generated automatically: `{chapterRoman}` produces `I`, `VIII`, `XXVI`. These variants are useful for sites that anchor sections by Roman chapter, for example Wikisource's `#I:8` Dhammapada verses.
+4. **Cumulative `{verseGlobal}`**: for systems whose locators have numeric `chapter` and `verse` groups and also declare `chapter_sizes:` (see below), the compiler exposes a global 1..N verse counter. This is useful for single-page resolvers, for example SuttaCentral's `/dhp8` and `/dhp102`, whose URLs use one running index across all chapters.
+5. **Provider-specific spellings**, declared per resolver with `vars:`. See below.
-If a template references a variable that doesn't exist for a given reference, the compiler skips that resolver entry for that reference and warns. Empty `resolver_targets` arrays are valid; references stay registered.
+If a template references a variable that does not exist for a given reference, the compiler skips that resolver entry for that reference and warns. Empty `resolver_targets` arrays are valid. References stay registered.
## When a provider spells a locator value differently
-A locator carries one canonical vocabulary — for `bible-book-chapter-verse` that is the [OSIS book codes](https://wiki.crosswire.org/OSIS_Book_Abbreviations) (`Gen`, `John`, `1Cor`). Providers do not all agree with it: die-bibel.de addresses the same books with USFM codes (`GEN`, `JHN`, `1CO`). `vars:` declares that translation for one resolver, leaving the rest of the template intact:
+A locator carries one canonical vocabulary. For `bible-book-chapter-verse` that is the [OSIS book codes](https://wiki.crosswire.org/OSIS_Book_Abbreviations), for example `Gen`, `John`, `1Cor`. Providers do not all agree with it: die-bibel.de addresses the same books with USFM codes, for example `GEN`, `JHN`, `1CO`. `vars:` declares that translation for one resolver, and leaves the rest of the template intact:
```yaml
- provider: Deutsche Bibelgesellschaft
@@ -153,15 +169,15 @@ A locator carries one canonical vocabulary — for `bible-book-chapter-verse` th
url: 'https://www.die-bibel.de/bibel/NA28/{bookUsfm}.{chapter}/#{bookUsfm}.{chapter}.{verse}'
```
-Each entry takes the value of `from` and looks it up in `map`, binding the result to a new variable usable anywhere in `url` or as the `url_by` key. The name must be new: a `vars` name that shadows a locator capture group is rejected, so `{book}` always means the canonical code no matter which resolver you are reading.
+Each entry takes the value of `from` and looks it up in `map`. The result becomes a new variable, usable anywhere in `url` or as the `url_by` key. The name must be new. A `vars` name that shadows a locator capture group is rejected, so `{book}` always means the canonical code, no matter which resolver you read.
-A value with no entry in the map is treated exactly like a missing template variable — the entry is skipped and the compiler warns. A hole in a book table therefore shows up in the build output instead of quietly producing a wrong URL, so watch the skipped-entry count when filling one in.
+A value with no entry in the map is treated like a missing template variable: the entry is skipped and the compiler warns. A hole in a book table therefore shows up in the build output, instead of quietly producing a wrong URL. Watch the skipped-entry count when you fill one in.
-Reach for `vars:` when a provider renames _part_ of the URL; reach for `url_by:` (below) when the whole URL is arbitrary.
+Use `vars:` when a provider renames part of the URL. Use `url_by:` (described below) when the whole URL is arbitrary.
## When a URL pattern isn't templatable
-Some providers use chapter or section names that don't fit a formula (e.g. `01-Pairs.htm`, `02-Heedfulness.htm`). For those, replace `url:` with a per-key map:
+Some providers use chapter or section names that do not fit a formula, for example `01-Pairs.htm` or `02-Heedfulness.htm`. For those providers, replace `url:` with a per-key map:
```yaml
- provider: ancient-buddhist-texts.net
@@ -173,11 +189,11 @@ Some providers use chapter or section names that don't fit a formula (e.g. `01-P
2: 'https://ancient-buddhist-texts.net/Texts-and-Translations/Dhammapada/02-Heedfulness.htm'
```
-The compiler looks up the value of the chosen variable (`chapter`) in the map and uses the matching URL. References for chapters not in the map are silently skipped for this provider — finish the map at your own pace.
+The compiler looks up the value of the chosen variable, `chapter`, in the map and uses the matching URL. A chapter not in the map is treated like a missing template variable: the entry is skipped and the compiler warns. Finish the map at your own pace, and watch the skipped-entry count.
## When even that isn't enough
-For one-off URLs that don't fit any pattern (the typical case for older citation systems like Stephanus or Bekker pagination), put the URL directly on the reference using `extra_resolvers`:
+For one-off URLs that do not fit any pattern, put the URL directly on the reference using `extra_resolvers`. This is the typical case for older citation systems, for example Stephanus or Bekker pagination.
```yaml
references:
@@ -195,7 +211,7 @@ Top-level `resolvers:` and per-reference `extra_resolvers:` both contribute to t
## Enumerating canonical reference sets
-Hand-listing every verse of Genesis or every line of the Iliad is not the right shape for a YAML file. For works whose reference set is regular enough to describe in a few numbers, use `references_range:` instead of (or alongside) `references:`. Each entry is one named expander; the compiler concatenates every expansion with the explicit `references:` list, de-dupes, and validates each generated locator against the citation system's regex.
+Hand-listing every verse of Genesis or every line of the Iliad is not the right shape for a YAML file. For works whose reference set is regular enough to describe in a few numbers, use `references_range:` instead of, or alongside, `references:`. Each entry is one named expander. The compiler concatenates every expansion with the explicit `references:` list, removes duplicates, and validates each generated locator against the citation system's regex.
```yaml
# 81 references: '1', '2', …, '81'
@@ -320,17 +336,17 @@ references_range:
page_range: [327, 621]
```
-Multiple `references_range` entries on one work are concatenated. Combine with explicit `references:` entries for one-off locators that don't fit any range.
+Multiple `references_range` entries on one work are concatenated. Combine them with explicit `references:` entries for one-off locators that do not fit any range.
## Additional citation systems and reference_status
-The top-level `citation_system:` block is the work's **preferred** system: its references also get the bare `/cite/{work_key}/{locator}/` alias, and it is what `Work.preferred_citation_system_key` points at in the compiled record. A work MAY additionally carry `additional_systems:` — a list of fallback blocks, each with its own `citation_system:`, `resolvers:`, `references:`, and `references_range:`, scoped exactly like the top-level block:
+The top-level `citation_system:` block is the work's **preferred** system. Its references also get the bare `/cite/{work_key}/{locator}/` alias. It is also what `Work.preferred_citation_system_key` points at in the compiled record. A work MAY carry `additional_systems:` as well: a list of fallback blocks, each with its own `citation_system:`, `resolvers:`, `references:`, and `references_range:`, scoped exactly like the top-level block:
```yaml
work:
key: plato.republic
preferred_label: Republic
- status: active
+ status: draft
created: 2026-05-31
modified: 2026-05-31
@@ -339,19 +355,23 @@ references:
- '514a'
additional_systems:
- - citation_system: book-chapter # fallback system
+ - citation_system: book-chapter # fallback system — must already exist as data/systems/book-chapter.yaml
reference_status: draft # optional; explicit here for clarity
references:
- '7.1'
```
-The status default is **asymmetric**: the top-level block's `reference_status` defaults to the work's own `status`, but an `additional_systems` block's `reference_status` defaults to `draft` — never to the work's status. Adding a fallback system to an already-active work never silently promotes its references to `active`; each fallback is reviewed on its own. Declaring the same `citation_system` twice for one work — as the preferred system and again under `additional_systems`, or twice within `additional_systems` — is rejected when the source file is parsed.
+`book-chapter` here is illustrative only. Every `citation_system` key must already be registered under `data/systems/`, whether at the top level or inside `additional_systems`. An unregistered key is a build-time error: the compiler throws `references unknown citation_system "…"` for that work.
+
+The status default is **asymmetric**. The top-level block's `reference_status` defaults to the work's own `status`. An `additional_systems` block's `reference_status` defaults to `draft` instead, never to the work's status. Adding a fallback system to an already-active work never silently promotes its references to `active`. Each fallback is reviewed on its own.
-Resolver URL templates stay scoped to their block, because their template variables come from that block's own citation system's `locator_regex` capture groups.
+Declaring the same `citation_system` twice for one work is rejected when the source file is parsed. This applies whether it appears as the preferred system and again under `additional_systems`, or twice within `additional_systems`.
+
+Resolver URL templates stay scoped to their own block. Their template variables come from the `locator_regex` capture groups of that block's citation system.
## Citation system files
-A citation system declares its locator format once and is reused by every work that cites it.
+A citation system declares its locator format once. Every work that cites it reuses that declaration.
```yaml
# data/systems/dhammapada-chapter-verse.yaml
@@ -398,7 +418,10 @@ created: 2026-05-31
modified: 2026-06-01
```
-Name your capture groups deliberately — every URL template in every work that uses this system can refer to them. Add `chapter_sizes:` only when (a) the locator has numeric `chapter` and `verse` groups and (b) at least one resolver actually needs the global counter.
+Name your capture groups deliberately. Every URL template in every work that uses this system can refer to them. Add `chapter_sizes:` only when both conditions hold:
+
+- the locator has numeric `chapter` and `verse` groups;
+- at least one resolver needs the global counter.
## Building, validating, and previewing
@@ -409,16 +432,16 @@ npm run build:data # both, in order
npm run dev # browse at http://localhost:4321/reg/ ; records live under /id/
```
-The compiler is deterministic: re-running `compile:data` against unchanged source produces zero diff. `MappingAssertion` and `CanonicalReference` UUIDs are derived from content per [Identifier syntax](/standard/identifier-syntax/), so the same YAML always produces the same identifiers.
+The compiler is deterministic. Re-running `compile:data` against unchanged source produces zero diff. `MappingAssertion` and `CanonicalReference` UUIDs are derived from content, per [Identifier syntax](/standard/identifier-syntax/). The same YAML therefore always produces the same identifiers.
## What lives where
-- `/id/work/{key}/` — a Work's canonical landing page (mappings, references, citation systems). A sibling `/id/work/{key}.json` serves the same record as JSON-LD.
-- `/id/system/{key}/` — a CitationSystem's canonical landing page (regex, examples, references). Plus `/id/system/{key}.json`.
-- `/id/ref/{uuid}/` — a CanonicalReference page with every resolver URL grouped by language. Plus `/id/ref/{uuid}.json`.
-- `/id/mapping/{uuid}/` — a MappingAssertion page. Plus `/id/mapping/{uuid}.json`.
-- `/reg/` — the human registry browser (filter works and citation systems, then browse paginated reference lists from work/system pages).
-- `/cite/{work_key}/{citation_system_key}/{locator}/` — qualified short alias, minted for every reference.
-- `/cite/{work_key}/{locator}/` — bare short alias, minted only for a work's preferred citation system; it MAY be retargeted if that preference changes.
+- `/id/work/{key}/`: a Work's canonical landing page (mappings, references, citation systems). A sibling `/id/work/{key}.json` serves the same record as JSON-LD.
+- `/id/system/{key}/`: a CitationSystem's canonical landing page (description, regex, works using it with reference counts). Plus `/id/system/{key}.json`.
+- `/id/ref/{uuid}/`: a CanonicalReference page with every resolver URL grouped by language. Plus `/id/ref/{uuid}.json`.
+- `/id/mapping/{uuid}/`: a MappingAssertion page. Plus `/id/mapping/{uuid}.json`.
+- `/reg/`: the human registry browser (filter works and citation systems, then browse paginated reference lists from work pages).
+- `/cite/{work_key}/{citation_system_key}/{locator}/`: qualified short alias, minted for every reference.
+- `/cite/{work_key}/{locator}/`: a bare short alias, minted only for a work's preferred citation system. It MAY be retargeted if that preference changes.
-A reader who types `https://textrefs.org/cite/plato.republic/stephanus/514a` (qualified) or `https://textrefs.org/cite/plato.republic/514a` (bare) lands on the same canonical reference page; the alias index is generated alongside the records by the compiler. See [URL layout](/get-started/url-layout/) for the full four-prefix model and alias-permanence rules.
+A reader who types `https://textrefs.org/cite/plato.republic/stephanus/514a` (qualified) or `https://textrefs.org/cite/plato.republic/514a` (bare) lands on the same canonical reference page. The compiler generates the alias index alongside the records. See [URL layout](/get-started/url-layout/) for the full four-prefix model and alias-permanence rules.
diff --git a/src/content/docs/get-started/how-it-works.md b/src/content/docs/get-started/how-it-works.md
index 5aecfc0..d53da6b 100644
--- a/src/content/docs/get-started/how-it-works.md
+++ b/src/content/docs/get-started/how-it-works.md
@@ -11,7 +11,7 @@ The core move is simple: separate the cited passage from the places where someon
## The four records
-TextRefs uses three records for identity and one record for work-level equivalences. Reading locations are embedded directly on the `CanonicalReference`.
+TextRefs uses three records for identity and one record for work-level mappings. Reading locations are embedded directly on the `CanonicalReference`.
| Record | Question it answers | Example |
| -------------------- | --------------------------------- | --------------------------------------------- |
@@ -20,7 +20,7 @@ TextRefs uses three records for identity and one record for work-level equivalen
| `CanonicalReference` | Which point inside the work? | `514a`, with embedded `resolver_targets` |
| `MappingAssertion` | What else identifies this _work_? | a CTS URN, Wikidata item, or DOI for the work |
-`MappingAssertion.subject` is always a `Work` IRI. Passage-level external identifiers are derived from the work-level mapping plus the locator; they are not stored one per passage. See [the specification](/standard/specification/) for the normative rules.
+`MappingAssertion.subject` is always a `Work` IRI. Passage-level external identifiers are derived from the work-level mapping plus the locator. The registry does not store them one per passage. See [the specification](/standard/specification/) for the normative rules.
## From citation string to TextRefs ID
@@ -31,7 +31,12 @@ For a citation such as "Plato, _Republic_ 514a", a registry contributor or parse
3. Normalize the locator according to that citation system: `514a`.
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.
+The result is a persistent TextRefs URI for the cited point itself. That URI stays stable even when:
+
+- a website changes its URLs;
+- a library adds a new scan;
+- a translation appears;
+- a mapping is corrected.
## Example: Plato, Republic 514a
@@ -51,18 +56,18 @@ The identity is the Stephanus reference, not any one Greek text, translation, or
"edition": "Plato, Republic (Burnet, OCT)",
"provider": "Perseus Digital Library",
"access": "open",
- "license": "CC-BY-SA-3.0",
+ "license": "https://spdx.org/licenses/CC-BY-SA-3.0",
"license_url": "https://www.perseus.tufts.edu/hopper/opensource",
"last_checked": "2026-06-03"
}
],
- "status": "active",
+ "status": "draft",
"created": "2026-05-31",
- "modified": "2026-06-03"
+ "modified": "2026-08-12"
}
```
-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:
+When reading locations exist, the registry embeds them 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 resolver targets to STEP Bible and Deutsche Bibelgesellschaft:
```json
{
@@ -73,22 +78,32 @@ When reading locations exist, they are embedded on the reference itself, one ent
"locator": "John.3.16",
"resolver_targets": [
{
- "url": "https://www.stepbible.org/?q=version=SBLG|reference=John.3.16",
+ "url": "https://www.stepbible.org/?q=version=SBLG%7Creference=John.3.16",
"language": "grc",
"edition": "SBL Greek New Testament",
"provider": "STEP Bible",
"access": "open",
- "license": "CC-BY-4.0",
- "license_url": "https://sblgnt.com/license/"
+ "license": "https://spdx.org/licenses/CC-BY-4.0",
+ "license_url": "https://sblgnt.com/license/",
+ "last_checked": "2026-06-03"
+ },
+ {
+ "url": "https://www.die-bibel.de/bibel/NA28/JHN.3/#JHN.3.16",
+ "language": "grc",
+ "edition": "Nestle-Aland, Novum Testamentum Graece, 28th edn (NA28)",
+ "provider": "Deutsche Bibelgesellschaft",
+ "access": "open",
+ "license_url": "https://www.die-bibel.de/impressum",
+ "last_checked": "2026-08-12"
}
],
- "status": "active",
+ "status": "draft",
"created": "2026-05-31",
- "modified": "2026-05-31"
+ "modified": "2026-08-12"
}
```
-Work-level equivalences live in a `MappingAssertion`:
+Work-level mappings live in a `MappingAssertion`. This one says the Wikidata item denotes the same work:
```json
{
@@ -100,13 +115,13 @@ Work-level equivalences live in a `MappingAssertion`:
"conforms_to": "https://www.wikidata.org/"
},
"source": "manual-curation",
- "status": "active",
+ "status": "draft",
"created": "2026-05-31",
- "modified": "2026-06-02"
+ "modified": "2026-08-11"
}
```
-Adding a resolver target adds one entry to `resolver_targets`; adding a Wikidata QID adds one `MappingAssertion`. No new records are minted per passage. A reference with no curated reading URL yet is still a valid identity record.
+Adding a resolver target adds one entry to `resolver_targets`. Adding a Wikidata QID adds one `MappingAssertion`. The compiler mints no new records per passage. A reference with no curated reading URL yet is still a valid identity record.
## Example: John.3.16
@@ -123,19 +138,25 @@ For a heavily translated work, many locations can share one reference identity.
An English translation, a German translation, a Greek edition, and a library scan can all sit in the `resolver_targets` array on the same reference. Adding a new translation adds another entry, not another canonical reference.
-For complete worked examples, see the live [Dhammapada work page](/id/work/dhammapada/) (four providers, two languages, 423 references) or the [Plato _Republic_ work page](/id/work/plato.republic/) (Stephanus pagination). The contributor YAML behind them is documented in [Authoring registry data](/get-started/authoring/).
+For complete worked examples, see the live [Dhammapada work page](/id/work/dhammapada/), which has three resolver targets across two providers, two languages, and 423 references. Alternatively, see the [Plato _Republic_ work page](/id/work/plato.republic/), which uses Stephanus pagination. The contributor YAML behind them is documented in [Authoring registry data](/get-started/authoring/).
-Where traditions number passages differently, create separate references under separate citation systems. Do not collapse divergent versification, pagination, or segmentation into one identity. The equivalence between the citation systems themselves is not yet expressible in this version: `MappingAssertion.subject` MUST be a Work IRI, so a system-to-system assertion cannot be authored. A future revision may widen `subject` to admit a `CitationSystem` IRI.
+Where traditions number passages differently, create separate references under separate citation systems. Do not collapse divergent versification, pagination, or segmentation into one identity. The equivalence between the citation systems themselves is not yet expressible in this version. `MappingAssertion.subject` MUST be a Work IRI, so a system-to-system assertion cannot be authored. A future revision may widen `subject` to admit a `CitationSystem` IRI.
## What TextRefs does not store
-TextRefs stores reference data, not texts. Registry records must not include full text, translations, critical apparatus, commentary, or copyrighted edition content.
+TextRefs stores reference data, not texts. Registry records must not include:
+
+- full text;
+- translations;
+- critical apparatus;
+- commentary;
+- copyrighted edition content.
Keep those in editions, libraries, repositories, or reading platforms. TextRefs only records the stable reference identity, curated mappings, resolver targets, and provenance needed to connect those systems.
## Keep reading
-- [Authoring registry data](/get-started/authoring/) — the contributor YAML format and the `npm run build:data` pipeline.
+- [Authoring registry data](/get-started/authoring/) documents the contributor YAML format and the `npm run build:data` pipeline.
- [Mappings and resolver targets](/get-started/mappings-and-resolver-targets/) explains how to decide whether an external resource should be modelled as a `MappingAssertion` or a resolver-target entry.
-- [Related identifier systems](/get-started/related-systems/) compares TextRefs with DOI, ARK, CTS, DTS, Wikidata, VIAF, TEI, and platform URLs.
+- [Related standards and systems](/get-started/related-systems/) groups the neighbouring standards by the layer they address, from identifier schemes to annotation models and reading platforms.
- [The standard](/standard/) contains the normative object model and validation rules.
diff --git a/src/content/docs/get-started/index.md b/src/content/docs/get-started/index.md
index 9d382af..64d419a 100644
--- a/src/content/docs/get-started/index.md
+++ b/src/content/docs/get-started/index.md
@@ -11,13 +11,13 @@ A passage has one identity. The editions that carry it are many.
To software it is invisible. The number lives as plain text in a footnote, dependent on a reader who knows the convention. No tool can resolve it, no link can carry it, no pipeline can index it. A reference that survived four hundred years on paper breaks in a decade online, because the edition behind it sits in a repository the citation cannot reach.
-TextRefs closes that gap. Every canonical reference is minted as a persistent identity of its own, a single HTTP URI for the passage a scholar means. Editions, translations, corpora and databases attach to it as curated mappings: a Stephanus locator, a CTS URN, a Wikidata QID, a DOI for the Loeb, the reading URL of the archive that holds the text. The citation becomes the doorway, and everything that carries the passage is reachable through it.
+TextRefs closes that gap. Every canonical reference is minted as a persistent identity of its own, a single HTTP URI for the passage a scholar means. Editions, translations, corpora and databases attach to it: identifiers that name the same work — a CTS URN, a Wikidata QID, a DOI for the Loeb — and the reading URL that resolves this one passage in the archive that holds the text. The citation becomes the doorway, and everything that carries the passage is reachable through it.
This is the interoperability scholarship has lacked. Every scholar already keeps the map privately. Bekker for the _Metaphysics_, Corcilius for the _De anima_, Rashed for _On Generation and Corruption_. Exact, hard-won, and gone the moment the article closes. TextRefs makes it shared and machine-readable. Oxford and the Loeb, Leipzig and Perseus, Wikidata and VIAF keep their own identifiers, their own homes, their own richer work, joined through the one reference they share. No central host. No redundancy. Authority stays with the institutions that earned it, and the archive that digitised an edition is now one mapping away from every citation of the passage it holds.
-The division is deliberate. TextRefs holds the reference layer only and nothing else. It hosts no text, replaces no edition, claims no apparatus. The same model covers every field that cites by structure: a Stephanus passage in Plato, a Bekker line in Aristotle, an article in the _Summa_, a chapter and verse in the Vulgate, a fragment in the _Digesta_. Each citation system carries its own locator rules. The registry stays small, persistent and legally reusable, released under CC0 so the data flows into any tool that needs it, curated by scholars through reviewed contributions, governed as non-profit infrastructure, not owned by a press.
+The division is deliberate. TextRefs holds the reference layer only and nothing else. It hosts no text, replaces no edition, claims no apparatus. The same model covers every field that cites by structure: a Stephanus passage in Plato, a Bekker line in Aristotle, an article in the _Summa_, a chapter and verse in the Vulgate, a fragment in the _Digesta_. Each citation system carries its own locator rules. The registry stays small, persistent and legally reusable. It is released under CC0 so the data flows into any tool that needs it, curated by scholars through reviewed contributions, and governed as non-profit infrastructure, not owned by a press.
-Four record types carry the model. `Work`, `CitationSystem`, `CanonicalReference`, and `MappingAssertion` for equivalence, published as JSON-LD against SKOS, Dublin Core and schema.org. Existing systems are layered, never displaced. A DOI still names the edition. A CTS URN still names the passage in Perseus. TextRefs holds the canonical reference they share, and makes it resolve.
+Four record types carry the model: `Work`, `CitationSystem`, `CanonicalReference`, and `MappingAssertion` for curated relations. TextRefs publishes all four as JSON-LD against SKOS, Dublin Core, PROV-O, and schema.org. Existing systems are layered, never displaced. A DOI still names the edition. A CTS URN still names the passage in Perseus. TextRefs holds the canonical reference they share, and makes it resolve.
[Browse the registry](/reg/). [Read the standard](/standard/). [Bring your corpus in](/get-started/authoring/).
@@ -27,14 +27,14 @@ Four record types carry the model. `Work`, `CitationSystem`, `CanonicalReference
- [Use cases](/get-started/use-cases/) — concrete scenarios across research, libraries, digital editions, and AI grounding.
- [Mappings and resolver targets](/get-started/mappings-and-resolver-targets/) — how to model external identifiers, reading URLs, and canonical-citation candidates.
- [Authoring registry data](/get-started/authoring/) — the contributor YAML format and the `npm run build:data` pipeline.
-- [Related identifier systems](/get-started/related-systems/) — how TextRefs relates to DOI, ARK, CTS, DTS, Wikidata, VIAF, and friends.
+- [Related standards and systems](/get-started/related-systems/) — how TextRefs relates to identifier schemes, text APIs, bibliographic models, edition and annotation standards, digital surrogates, and reading platforms.
- [URL layout](/get-started/url-layout/) — how `/id/`, `/reg/`, `/cite/`, and `/api/` fit together.
- [The standard](/standard/) — the normative specification text (`v0.1.0-draft`).
- [The association](/association/) — the non-profit behind TextRefs, its statutes, and the open board search.
## Live examples
-- [`/id/work/dhammapada/`](/id/work/dhammapada/) — Dhammapada with three real providers (SuttaCentral in Pali and English, ancient-buddhist-texts.net).
+- [`/id/work/dhammapada/`](/id/work/dhammapada/) — Dhammapada with three real resolver entries across two providers (SuttaCentral in Pali and English, ancient-buddhist-texts.net).
- [`/id/work/plato.republic/`](/id/work/plato.republic/) — Plato's _Republic_ with Stephanus pagination.
- [`/cite/plato.republic/514a`](/cite/plato.republic/514a) — a short alias that redirects to the canonical reference URL. The qualified form, [`/cite/plato.republic/stephanus/514a`](/cite/plato.republic/stephanus/514a), resolves to the same page.
- [`/reg/`](/reg/) — the registry browser.
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 93c9a5c..60737cd 100644
--- a/src/content/docs/get-started/mappings-and-resolver-targets.md
+++ b/src/content/docs/get-started/mappings-and-resolver-targets.md
@@ -7,7 +7,7 @@ sidebar:
This guide helps contributors decide whether an external resource belongs in a `MappingAssertion`, in the embedded `resolver_targets` array on a `CanonicalReference`, or neither.
-Use it after you have already identified the `Work`, the `CitationSystem`, and the `CanonicalReference` for the citation itself. For the underlying model, start with [How it works](/get-started/how-it-works/). For the authoring format, see [Authoring registry data](/get-started/authoring/).
+Use this guide after you identify the `Work`, the `CitationSystem`, and the `CanonicalReference` for the citation. For the underlying model, start with [How it works](/get-started/how-it-works/). For the authoring format, see [Authoring registry data](/get-started/authoring/).
## Quick rule
@@ -19,20 +19,20 @@ Ask what the external thing is doing.
| is a URL where a reader can inspect a specific passage | entry in `CanonicalReference.resolver_targets` |
| is only an author, institution, subject, or non-textual authority | usually not TextRefs |
-A `MappingAssertion` is about work-level equivalence. A `resolver_targets` entry is about dereferencing one passage.
+A `MappingAssertion` describes the work as a whole. A `resolver_targets` entry lets a reader dereference one specific passage.
## Use a MappingAssertion for work-level identifiers
-Create a `MappingAssertion` when another system has an identifier for the _whole work_ that should be connected to a TextRefs `Work`. The `subject` MUST be a Work IRI.
+Create a `MappingAssertion` when another system has an identifier for the _whole work_ and you want to connect it to a TextRefs `Work`. The `subject` MUST be a Work IRI.
Common mapping targets include:
-- Wikidata QIDs for works;
-- CTS URNs at the work level (e.g. `urn:cts:greekLit:tlg0059.tlg030`);
-- DOIs, Handles, ARKs, PURLs, or URN:NBNs for editions or digital objects;
+- Wikidata QIDs for works.
+- CTS URNs at the work level, for example `urn:cts:greekLit:tlg0059.tlg030`.
+- DOIs, Handles, ARKs, PURLs, or URN:NBNs for editions or digital objects.
- another TextRefs Work when two registries need to be aligned.
-Choose the relation by what the target is, not by how confident you feel: use `alternateOf` when the target is another entity denoting the same work (e.g. a Wikidata item). Use `isReferencedBy` when the target is a document or page about the work (e.g. a Wikipedia article).
+Choose the relation by what the target is, not by how confident you feel. Use `alternateOf` when the target is another entity that denotes the same work, for example a Wikidata item. Use `isReferencedBy` when the target is a document or page about the work, for example a Wikipedia article.
```json
{
@@ -40,14 +40,14 @@ Choose the relation by what the target is, not by how confident you feel: use `a
"subject": "https://textrefs.org/id/work/dhammapada",
"relation": "alternateOf",
"target": {
- "identifier": "https://www.wikidata.org/entity/Q220114",
+ "identifier": "https://www.wikidata.org/entity/Q748878",
"conforms_to": "https://www.wikidata.org/"
},
"source": "manual-curation"
}
```
-Passage-level external identifiers (e.g. the CTS URN for `John.3.16`) are not stored as records. They are derived from the work-level mapping plus the locator at resolve time.
+Passage-level external identifiers, for example the CTS URN for `John.3.16`, are not stored as records. They are derived from the work-level mapping plus the locator at resolve time.
## Add resolver targets for reading locations
@@ -55,10 +55,10 @@ A `resolver_targets` entry is a dereferenceable URL where a reader can inspect t
Typical resolver targets include:
-- a Perseus or Scaife page for a Greek or Latin passage;
-- a Wikisource page or section anchor;
-- a Bible Gateway, Sefaria, Quran.com, or similar reading URL;
-- an institutional repository page for a digitized edition;
+- a Perseus or Scaife page for a Greek or Latin passage.
+- a Wikisource page or section anchor.
+- a Bible Gateway, Sefaria, Quran.com, or similar reading URL.
+- an institutional repository page for a digitized edition.
- a licensed platform URL, if the access status is recorded honestly.
Each entry records what a reader needs to understand the link: `language` (BCP 47), `edition`, `provider`, `access`, `license` when known, and `last_checked` when maintained.
@@ -88,11 +88,11 @@ Each entry records what a reader needs to understand the link: `language` (BCP 4
}
```
-Contributors author resolver targets as URL templates, not raw URLs — see [Authoring registry data](/get-started/authoring/). The compiler expands them per reference. Browse the live output at [`/id/work/dhammapada/`](/id/work/dhammapada/) or [`/id/work/plato.republic/`](/id/work/plato.republic/).
+Contributors author resolver targets as URL templates, not raw URLs. See [Authoring registry data](/get-started/authoring/) for the format. The compiler expands them per reference. Browse the live output at [`/id/work/dhammapada/`](/id/work/dhammapada/) or [`/id/work/plato.republic/`](/id/work/plato.republic/).
## 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 a documented canonical locator form, a `locator_regex`, 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 |
| ---------- | ---------------------------------------- | -------------------- | ------------------------------------------------------------- |
@@ -107,17 +107,21 @@ The best TextRefs candidates are works with established, edition-independent cit
| Classics | Homer, _Iliad_ `1.1` | book and line | Present as `homer.iliad` with `homer-book-line`. |
| Classics | Homer, _Odyssey_ `1.426` | book and line | Present as `homer.odyssey` with `homer-book-line`. |
-An author's name alone is not a `Work`. For example, "Confucius" is an authority or attribution problem; _Analects_ is the textual work that can receive canonical references.
+An author's name alone is not a `Work`. For example, "Confucius" is an authority or attribution problem. _Analects_ is the textual work that can receive canonical references.
## Edge cases
-**Edition page numbers.** Page numbers from one printed edition usually belong to that edition. They can support a resolver target or an edition-level mapping, but they should not become a canonical citation system unless the community actually cites the work that way across editions.
+**Edition page numbers.** Page numbers from one printed edition usually belong to that edition. They can support a resolver target or an edition-level mapping. They should not become a canonical citation system unless the community cites the work that way across editions.
**Translations.** A translation is a resolver target when it lets readers inspect the cited passage. It is not a new canonical reference unless the translation has its own independently cited segmentation.
-**Divergent numbering.** If two traditions number the same material differently, create separate `CanonicalReference`s under separate `CitationSystem`s. The equivalence between the two citation systems is not yet expressible in this version: `MappingAssertion.subject` MUST be a Work IRI, so a system-to-system assertion cannot be authored. A future revision may widen `subject` to admit a `CitationSystem` IRI.
+**Divergent numbering.** If two traditions number the same material differently, create separate `CanonicalReference`s under separate `CitationSystem`s. The equivalence between the two citation systems is not yet expressible in this version. `MappingAssertion.subject` MUST be a Work IRI, so a system-to-system assertion cannot be authored. A future revision may widen `subject` to admit a `CitationSystem` IRI.
-**Contained-by relationships.** If an identifier points to a whole edition, scan, or digital object rather than the exact passage, it is not a `MappingAssertion` target at all. If it is an alternate presentation of the whole work, model it as `alternateOf`; otherwise it belongs in `resolver_targets`.
+**Contained-by relationships.** An identifier for a whole edition, scan, or digital object is still chosen by what it denotes, per the [ADR-0006](https://github.com/textrefs/textrefs.org/blob/main/decisions/ADR-0006-mapping-relation-vocabulary.md) test:
+
+- if it denotes the same work, model it as `alternateOf`.
+- if it is a document about the work, model it as `isReferencedBy`.
+- if it only lets a reader inspect one specific passage, it belongs in `resolver_targets`, not a mapping.
**Unstable websites.** A website URL can be useful as a resolver target even if it is not a stable identifier. Do not derive TextRefs IDs from it.
@@ -125,12 +129,12 @@ 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 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 `relation` matches what the target actually is (`alternateOf` for a same-work entity, `isReferencedBy` for a document about the work);
-- each `resolver_targets` entry has a dereferenceable URL and honest access metadata;
-- the proposal documents its source or curation basis;
+- 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 `relation` matches what the target is (`alternateOf` for a same-work entity, `isReferencedBy` for a document about the work).
+- each `resolver_targets` entry has a dereferenceable URL and honest access metadata.
+- the proposal documents its source or curation basis.
- no full text, translation text, apparatus, or commentary is copied into the registry.
See [Contributing](/community/contributing/) for review tracks and pull-request expectations.
diff --git a/src/content/docs/get-started/related-systems.md b/src/content/docs/get-started/related-systems.md
index 7eef875..ccecbfe 100644
--- a/src/content/docs/get-started/related-systems.md
+++ b/src/content/docs/get-started/related-systems.md
@@ -1,41 +1,113 @@
---
-title: Related identifier systems
-description: How TextRefs relates to DOI, ARK, Handle, PURL, CTS, DTS, Wikidata, VIAF, TEI, and platform URLs.
+title: Related standards and systems
+description: How TextRefs relates to identifier schemes, text APIs, bibliographic models, edition and annotation standards, digital surrogates, and reading platforms.
sidebar:
order: 5
---
-Use TextRefs for _canonical references inside a work_ — Stephanus 514a, Bekker 983b10, _ST_ I-II.94.2 — and use existing identifier systems for the editions, files, authority records, catalogues, and platforms that carry those references. The difference matters: you can cite Plato's _Republic_ 514a across a dozen editions and centuries without naming any specific edition, while a DOI identifies one published object at a time.
+Use TextRefs for _canonical references inside a work_, such as Stephanus 514a, Bekker 983b10, or _ST_ I-II.94.2. Use existing identifier systems for the editions, files, authority records, catalogues, and platforms that carry those references. The difference matters. You can cite Plato's _Republic_ 514a across a dozen editions and centuries without naming any specific edition. A DOI, by contrast, identifies one published object at a time.
-So the relationship is almost always: TextRefs holds the canonical reference, and the system in the table below is one of its mappings.
+The systems on this page work at different layers of textual reference. Very few of them compete with TextRefs. Some supply mapping targets for a `Work`. Others describe a different layer: the encoded edition, the digitized object, the annotation, or the bibliographic record. Each section below names one layer and states what TextRefs adds to it.
For the practical modelling distinction between external identifiers and reading URLs, see [Mappings and resolver targets](/get-started/mappings-and-resolver-targets/).
-## Comparison
-
-| System | Identifies | Granularity | TextRefs relationship |
-| :------------------------------------------------------------------------------ | :----------------------------------------------------------- | :--------------------- | :--------------------------------------------------------------------------------------------------------------------- |
-| **[DOI](https://www.doi.org/)** | a published digital object (article, edition, dataset) | publication | TextRefs records carry DOI mappings for editions hosting the cited passage |
-| **[Handle](https://www.handle.net/)** | any digital object with a Handle.net record | object | underlying tech for DOI; same mapping pattern as DOI |
-| **[ARK](https://arks.org/)** | any object an institution chooses to persist | object | similar role to Handle; same mapping pattern |
-| **[PURL](https://purl.archive.org/)** | a redirecting persistent URL | URL only | TextRefs IDs are themselves HTTP URIs that redirect; PURLs can target a TextRefs ID |
-| **[CTS URN](http://cts.informatik.uni-leipzig.de/Canonical_Text_Service.html)** | a passage in a canonical work, Perseus/Homer-Multitext model | passage | strongest semantic overlap; TextRefs records CTS URNs as mappings so CTS-aware tools can interoperate |
-| **[DTS API](https://distributed-text-services.github.io/specifications/)** | a discovery/retrieval API for texts using CTS-style URNs | service | downstream consumer — DTS implementations can resolve TextRefs IDs via mappings |
-| **[Wikidata QID](https://www.wikidata.org/)** | an abstract entity (work, person, event) | work / entity | `Work` records map to Wikidata QIDs; TextRefs IDs handle the references inside those works |
-| **[VIAF](https://viaf.org/)** | author and work authority records | work / person | mapping target for `Work` records; TextRefs IDs provide passage-level identity |
-| **[TEI `xml:id`](https://tei-c.org/)** | a local anchor inside a TEI document | edition-local | edition-internal; TextRefs `MappingAssertion`s can point at a specific TEI anchor in a published edition |
-| **Perseus / Scaife URLs** | a passage on a specific reading platform | platform-bound passage | surface as `resolver_targets` entries with provenance so readers can jump from a stable reference to a useful platform |
+## Canonical reference and text APIs
+
+These systems address the same layer as TextRefs: the passage inside a work.
+
+| System | Identifies | Granularity | TextRefs relationship |
+| :------------------------------------------------------------------------------ | :-------------------------------------------------------------- | :---------- | :----------------------------------------------------------------------------------------------------- |
+| **[CTS URN](http://cts.informatik.uni-leipzig.de/Canonical_Text_Service.html)** | a passage in a canonical work, Perseus/Homer-Multitext model | passage | strongest semantic overlap. TextRefs records CTS URNs as mappings, so CTS-aware tools can interoperate |
+| **[DTS API](https://distributed-text-services.github.io/specifications/)** | a discovery and retrieval API for texts that use CTS-style URNs | service | downstream consumer — DTS implementations can resolve TextRefs IDs via mappings |
+
+## Persistent object and publication identifiers
+
+These schemes name a published object: an edition, an article, a dataset, or a scan. A TextRefs record maps to them at work level. The standard lists the same schemes with example `conforms_to` values in [Appendix B of the specification](/standard/specification/#appendix-b-well-known-external-identifier-schemes-informative).
+
+| System | Identifies | Granularity | TextRefs relationship |
+| :---------------------------------------- | :----------------------------------------------------- | :---------- | :------------------------------------------------------------------------------------------------------------- |
+| **[DOI](https://www.doi.org/)** | a published digital object (article, edition, dataset) | publication | TextRefs records carry DOI mappings for editions that host the cited passage |
+| **[Handle](https://www.handle.net/)** | any digital object with a Handle.net record | object | underlying technology for DOI. Same mapping pattern as DOI |
+| **[ARK](https://arks.org/)** | any object that an institution chooses to persist | object | similar role to Handle. Same mapping pattern |
+| **[PURL](https://purl.archive.org/)** | a redirecting persistent URL | URL only | TextRefs `/id/` URLs are the stable target and do not redirect (`/cite/` does). PURLs can target a TextRefs ID |
+| **[URN:NBN](https://nbn-resolving.org/)** | a digital object registered by a national library | object | national-library counterpart to Handle and ARK. Same mapping pattern |
+
+## Bibliographic and authority models
+
+These models describe works, agents, and records in libraries. Wikidata and VIAF supply mapping targets today. BIBFRAME and IFLA LRM show where TextRefs sits relative to library metadata.
+
+| System | Identifies | Granularity | TextRefs relationship |
+| :----------------------------------------------------------------- | :------------------------------------------------- | :-------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **[Wikidata QID](https://www.wikidata.org/)** | an abstract entity (work, person, event) | work / entity | `Work` records map to Wikidata QIDs. TextRefs IDs handle the references inside those works |
+| **[VIAF](https://viaf.org/)** | author and work authority records | work / person | mapping target for `Work` records. TextRefs IDs provide passage-level identity |
+| **[BIBFRAME](https://www.loc.gov/bibframe/)** | a bibliographic description (Work, Instance, Item) | work / instance | complementary vocabulary. Its Work level sits near a TextRefs `Work`; its Instance level is what a DOI or an ARK names |
+| **[IFLA LRM](https://repository.ifla.org/handle/20.500.14598/40)** | a conceptual model for bibliographic information | model | no identifiers of its own. Its work-to-manifestation split is the split TextRefs makes between a canonical reference and the editions that carry it |
+
+## Edition and fragment addressing
+
+These standards point into one representation of a text: one encoded file, one rendering, one fragment.
+
+| System | Identifies | Granularity | TextRefs relationship |
+| :---------------------------------------------------------------- | :--------------------------------------------------------- | :------------------------------- | :------------------------------------------------------------------------------------------------------------ |
+| **[TEI `xml:id`](https://tei-c.org/)** | a local anchor inside a TEI document | edition-local | edition-internal. A TEI anchor for a specific passage belongs in `resolver_targets`, not a `MappingAssertion` |
+| **[W3C Web Annotation](https://www.w3.org/TR/annotation-model/)** | an annotation, and the target it anchors through selectors | representation-specific fragment | complementary. Use a TextRefs URI as the annotation target for the canonical passage |
+
+The two carry different claims:
+
+- A **TextRefs URI** is the canonical semantic reference. It holds no matter which edition, file, or rendering you open.
+- A **Web Annotation selector** is a representation-specific location. It holds for the one text it was anchored to.
+
+IIIF builds on the Web Annotation model, so the same division applies to the next section.
+
+## Digital surrogates
+
+IIIF describes the digitized object and its parts, not the canonical passage.
+
+| System | Identifies | Granularity | TextRefs relationship |
+| :----------------------------------------------------------------- | :--------------------------------------------------------- | :---------------------- | :--------------------------------------------------------------------------------------------------------------- |
+| **[IIIF Presentation API](https://iiif.io/api/presentation/3.0/)** | a digitized object and its parts (Manifest, Canvas, Range) | digital object / canvas | complementary. A IIIF resource that shows one passage belongs in `resolver_targets`, not in a `MappingAssertion` |
+
+The questions differ:
+
+- **TextRefs** answers: which canonical passage is this?
+- **IIIF** answers: where does that passage appear in this digitized edition?
+
+TextRefs already borrows one convention from this layer. [ADR-0001](https://github.com/textrefs/textrefs.org/blob/main/decisions/ADR-0001-conforms-to-replaces-target-kind.md) adopted it from the Linked Art digital integration model, where the value points at a specification such as the IIIF profile URI.
+
+## Editorial conceptual models
+
+RAMEN models editorial objects and their relations. It is a recent model, and its own documentation states that it does not replace TEI or IIIF.
+
+| System | Identifies | Granularity | TextRefs relationship |
+| :--------------------------------------------- | :---------------------------------------------------------- | :---------- | :----------------------------------------------------------------------------------------------------------------- |
+| **[RAMEN](https://ramen-schema.org/concepts)** | editorial objects (Collection, Content, Annotation, Entity) | model | complementary. TextRefs can serve as the canonical-reference layer that a RAMEN `Annotation` or `Entity` refers to |
+
+That gives each layer one job:
+
+- **RAMEN** — editorial objects and their relations.
+- **TEI** — textual representation and encoding.
+- **IIIF** — digital objects and their presentation.
+- **TextRefs** — stable canonical references.
+
+## Platforms and resolvers
+
+A reading platform shows the passage. It does not identify the passage.
+
+| System | Identifies | Granularity | TextRefs relationship |
+| :------------------------ | :--------------------------------------- | :--------------------- | :--------------------------------------------------------------------------------------------------------------------- |
+| **Perseus / Scaife URLs** | a passage on a specific reading platform | platform-bound passage | surface as `resolver_targets` entries with provenance so readers can jump from a stable reference to a useful platform |
## Where DOIs fit
- **Use DOIs for publications.** A DOI is the right identifier for a published edition, article, dataset, or digital object.
-- **Use TextRefs for cited passages.** Plato's _Republic_ 514a is a canonical reference. The Loeb edition that contains it can have a DOI; the passage gets a TextRefs ID that can map to that DOI-backed edition.
-- **Layer identifiers instead of replacing them.** Scholarly tools already understand DOIs for editions. Adding TextRefs IDs for canonical references gives those tools passage-level precision without changing their publication-level identifiers.
+- **Use TextRefs for cited passages.** Plato's _Republic_ 514a is a canonical reference. The Loeb edition that contains it can have a DOI. The passage gets a TextRefs ID that can map to that DOI-backed edition.
+- **Layer identifiers instead of replacing them.** Scholarly tools already understand DOIs for editions. If you add TextRefs IDs for canonical references, those tools gain passage-level precision, and their publication-level identifiers stay the same.
- **Keep the model affordable.** Canonical-reference coverage grows into the millions. A lightweight open registry is the practical way to curate that graph at non-profit scale.
## What this means for implementers
- Treat TextRefs IDs as the **primary** identifier for a canonical reference.
-- Read external identifiers and resolver targets from the `mappings` array — they are enriched metadata, not the citation's identity.
-- Fall back to your own resolver chain: if no mapping exists for the user's preferred edition, link to a default mapping or to the TextRefs landing page.
-- When you publish your own data, attach a TextRefs ID alongside whatever you already issue. This is how the citation graph grows without anyone changing primary keys.
+- Read external identifiers from a `Work`'s `alternateOf` and `isReferencedBy` arrays, and reading URLs from `CanonicalReference.resolver_targets`. These are enriched metadata, not the citation's identity.
+- If you write annotations, target the TextRefs IRI when you mean the canonical passage. Keep your selectors, canvases, and TEI anchors for the representation you actually annotated.
+- If no mapping exists for the user's preferred edition, fall back to your own resolver chain. Link to a default mapping or to the TextRefs landing page.
+- When you publish your own data, attach a TextRefs ID alongside whatever you already issue. This is how the citation graph grows, and no one needs to change primary keys.
diff --git a/src/content/docs/get-started/url-layout.md b/src/content/docs/get-started/url-layout.md
index 6a7b3a3..835a04a 100644
--- a/src/content/docs/get-started/url-layout.md
+++ b/src/content/docs/get-started/url-layout.md
@@ -7,12 +7,12 @@ sidebar:
TextRefs uses four URL prefixes, each with one job. Together they make every registry record citeable, browsable, machine-readable, and short-linkable.
-| Prefix | Role | What lives there |
-| -------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `/id/` | **Identifier** | The canonical, persistent URL of every record. Each record is published twice: `/id/.../` (HTML) and a sibling `/id/....json` (JSON-LD). |
-| `/reg/` | **Browse** | The human registry browser: filter works and citation systems, then browse paginated reference lists from those record pages. Links into `/id/`. |
-| `/cite/` | **Cite** | Short, memorable URLs (`/cite/{work}/{system}/{locator}` always; bare `/cite/{work}/{locator}` for the work's preferred system) that redirect to the canonical `/id/` URL. Convenience only — bare aliases MAY be retargeted; `/id/` is permanent. |
-| `/api/` | **API docs** | The OpenAPI document describing the `/id/` URL contract, plus the JSON-LD `@context` at `/contexts/`. |
+| Prefix | Role | What lives there |
+| -------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `/id/` | **Identifier** | The canonical, persistent URL of every record. Each record is published twice: `/id/.../` (HTML) and a sibling `/id/....json` (JSON-LD). |
+| `/reg/` | **Browse** | The human registry browser: filter works and citation systems, then browse paginated reference lists from work pages. Links into `/id/`. |
+| `/cite/` | **Cite** | Short, memorable URLs (`/cite/{work}/{system}/{locator}` always, and bare `/cite/{work}/{locator}` for the work's preferred system) that redirect to the canonical `/id/` URL. Convenience only. Bare aliases MAY be retargeted. `/id/` is permanent. |
+| `/api/` | **API docs** | The OpenAPI document that describes the `/id/` URL contract, plus the JSON-LD `@context` at `/contexts/`. |
In one line:
@@ -27,10 +27,10 @@ Plato's _Republic_ 514a — the Stephanus passage where Socrates begins the Alle
- `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. Every reference has a qualified alias; the bare form exists only for the work's preferred citation system:
+- **Short alias** — a memorable, hand-typeable shortcut that redirects to the canonical URL. Every reference has a qualified alias. The bare form exists only for the work's preferred citation system:
- `https://textrefs.org/cite/plato.republic/stephanus/514a` (qualified — always minted)
- `https://textrefs.org/cite/plato.republic/514a` (bare — minted because Stephanus is Republic's preferred system)
-- **Machine contract** — the OpenAPI describing how `/id/` behaves, so a client knows it can append `.json` to any canonical URL:
+- **Machine contract** — the OpenAPI that describes how `/id/` behaves, so a client knows it can append `.json` to any canonical URL:
- `https://textrefs.org/api/`
## How machine clients discover the JSON
@@ -45,17 +45,17 @@ There is no `Accept`-header content negotiation. Every HTML record page advertis
/>
```
-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.
+A client either reads that `` tag, or 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.
This mirrors how arxiv.org publishes each paper at `/abs/{id}` and `/pdf/{id}` — two static URLs, two representations, no negotiation needed.
## Why four prefixes, not one
-Persistent-identifier systems separate concerns. DOI and ORCID each have a canonical resolver URL that _is_ the API, with documentation living at a stable but distinct path. W3ID and PURL add short-alias redirects on top. TextRefs follows the same pattern:
+Persistent-identifier systems separate concerns. DOI and ORCID each have a canonical resolver URL that _is_ the API. Their documentation lives at a stable but distinct path. W3ID and PURL add short-alias redirects on top. TextRefs follows the same pattern:
-- The identifier (`/id/`) is the contract — it must be persistent and stable across editions, providers, and resolver implementations.
-- The browser (`/reg/`) is the discovery surface — it can change shape and add features without breaking citations.
-- The alias (`/cite/`) is convenience — short URLs that resolve back to the canonical identifier via ``. The qualified form (`/cite/{work}/{system}/{locator}`) exists for every reference; the bare form (`/cite/{work}/{locator}`) exists only for a work's preferred system and MAY be retargeted if that preference changes — `/id/` never is.
-- The docs (`/api/`) describe the contract for anyone integrating against `/id/`.
+- The identifier (`/id/`) is the contract. It must be persistent and stable across editions, providers, and resolver implementations.
+- The browser (`/reg/`) is the discovery surface. It can change shape and add features, and citations do not break.
+- The alias (`/cite/`) is convenience. Short URLs resolve back to the canonical identifier via ``. The qualified form (`/cite/{work}/{system}/{locator}`) exists for every reference. The bare form (`/cite/{work}/{locator}`) exists only for a work's preferred system and MAY be retargeted if that preference changes. `/id/` is never retargeted.
+- The docs (`/api/`) describe the contract for anyone who integrates against `/id/`.
If you only remember one thing: **cite the `/id/` URL, browse from `/reg/`, share the `/cite/` shortcut, and read `/api/` to integrate.**
diff --git a/src/content/docs/get-started/use-cases.md b/src/content/docs/get-started/use-cases.md
index ac4d86f..c6db2d0 100644
--- a/src/content/docs/get-started/use-cases.md
+++ b/src/content/docs/get-started/use-cases.md
@@ -9,36 +9,36 @@ These scenarios are illustrative, not exhaustive. Each pairs "what happens today
## Researcher citing a passage across editions
-**Today.** You write "Plato, _Rep._ 514a" in your manuscript. Your reader either knows the Stephanus convention and resolves the citation themselves, or they don't. If they want to follow up, they pick whichever edition is at hand; there is no shared link.
+**Today.** You write "Plato, _Rep._ 514a" in your manuscript. Your reader either knows the Stephanus convention and resolves the citation themselves, or they don't. If they want to follow up, they pick whichever edition is at hand. There is no shared link.
-**With TextRefs.** You drop in `https://textrefs.org/id/ref/...` (or share the short alias `/cite/plato.republic/514a`). The reader resolves it to a landing page listing every curated resolver target — Perseus, Scaife, Loeb (where licensed), a Wikisource transcript — plus a JSON-LD record and downloadable CSL JSON for citation tools. Switching editions is a click, not a search.
+**With TextRefs.** You drop in `https://textrefs.org/id/ref/...` (or share the short alias `/cite/plato.republic/514a`). The reader resolves it to a landing page that lists every curated resolver target, for example Perseus, Scaife, Loeb where licensed, and a Wikisource transcript. The page also gives a JSON-LD record and downloadable CSL JSON for citation tools. You can switch editions with a click, not a search.
## Digital edition project linking to and from canonical references
-**Today.** You publish a critical edition with project-local IDs. Other projects link to your URLs; six months later you reorganise the site and every inbound link breaks. You email three colleagues and apologise.
+**Today.** You publish a critical edition with project-local IDs. Other projects link to your URLs. Six months later you reorganise the site, and every inbound link breaks. You email three colleagues and apologise.
-**With TextRefs.** You mint a TextRefs ID for each canonical reference your edition surfaces and link your project-local IDs to it. Inbound scholarly links can target the TextRefs URI; you control your internal URL scheme without breaking the citation graph.
+**With TextRefs.** You mint a TextRefs ID for each canonical reference that your edition surfaces, and link your project-local IDs to it. Inbound scholarly links can target the TextRefs URI. You control your internal URL scheme, and the citation graph does not break.
## Library or institutional repository indexing scholarly works
-**Today.** Your full-text indexer extracts citations from a PDF — "Aristotle, _Eth. Nic._ 1094a1" — as a string. It can match other strings literally but it can't cluster, can't infer the work, can't expand abbreviations to _Nicomachean Ethics_.
+**Today.** Your full-text indexer extracts citations from a PDF — "Aristotle, _Eth. Nic._ 1094a1" — as a string. It can match other strings literally. It cannot cluster them, infer the work, or expand abbreviations to _Nicomachean Ethics_.
**With TextRefs.** The indexer parses the citation against the `bekker` `CitationSystem` and stores the resulting TextRefs ID. Now you have FRBR-style work clustering for free, cross-corpus passage search, and authority alignment with Wikidata via mappings.
## Theologian or legal scholar working with traditional reference systems
-**Today.** Stephanus pagination, Bekker numbering, Homeric book-and-line references, and biblical book-chapter-verse — every tradition has its own implicit, untyped notation. There is no machine-readable contract for what is a valid citation in each system.
+**Today.** Every tradition has its own implicit, untyped notation, for example Stephanus pagination, Bekker numbering, Homeric book-and-line references, or biblical book-chapter-verse. There is no machine-readable contract for what is a valid citation in each system.
-**With TextRefs.** Each tradition is a `CitationSystem` with a documented locator regex, a normalisation rule, and a list of valid reference types. A parser can validate "Vulg. Gen. 1:1" or reject "ST I-II.300.99" because no registered canonical reference exists for that locator. The reference identifier is independent of any single edition.
+**With TextRefs.** Each tradition is a `CitationSystem` with a documented `locator_regex` and a `description` that spells out the canonical locator form. A parser can validate "Vulg. Gen. 1:1" or reject "ST I-II.300.99" because no registered canonical reference exists for that locator. The reference identifier is independent of any single edition.
## AI/LLM grounding and retrieval
**Today.** Language models cite "Plato, _Republic_ 514a" verbatim from training data, including hallucinated passages. Retrieval-augmented systems have nothing to retrieve _against_ at the passage level — only documents.
-**With TextRefs.** Each canonical reference is a structured anchor. Training pipelines can tag occurrences in source material; retrievers can ground generations against `https://textrefs.org/id/ref/...` URIs; verification tools can compare the model's claim to a known reference and flag drift.
+**With TextRefs.** Each canonical reference is a structured anchor. Training pipelines can tag occurrences in source material. Retrievers can ground generations against `https://textrefs.org/id/ref/...` URIs. Verification tools can compare the model's claim to a known reference and flag drift.
## Citation-managing tools and scholarly markdown
-**Today.** Citation managers store free-text "Rep. 514a" — searchable, but neither typed nor linkable. Hypothes.is annotations on canonical passages are tied to a specific edition's URL.
+**Today.** Citation managers store free-text such as "Rep. 514a". This text is searchable, but it is neither typed nor linkable. Hypothes.is annotations on canonical passages are tied to a specific edition's URL.
-**With TextRefs.** Citation managers store a TextRefs URI as the primary key; the human-readable label is just a display string. Annotation tools can normalise edition-bound URLs to TextRefs IDs so a marginal note on Plato 514a follows the passage rather than the edition.
+**With TextRefs.** Citation managers store a TextRefs URI as the primary key. The human-readable label is only a display string. Annotation tools can normalise edition-bound URLs to TextRefs IDs, so a marginal note on Plato 514a follows the passage rather than the edition.
diff --git a/src/content/docs/standard/index.md b/src/content/docs/standard/index.md
index ce50a13..c1c6b75 100644
--- a/src/content/docs/standard/index.md
+++ b/src/content/docs/standard/index.md
@@ -6,9 +6,9 @@ sidebar:
order: 1
---
-TextRefs defines a minimal registry standard for stable, machine-addressable references to texts. Its centre is the separation of **identity** from **location**: a reference such as `John.3.16` is one abstract, language-independent identity, while the translations, editions, and providers that carry it are recorded as locations embedded on the reference. `Work` and `CitationSystem` records use flat stable keys; richer bibliographic and authority data is connected through mappings to external systems, with optional lightweight `Work.creators` retained for citation rendering. The model has four registry object types — `Work`, `CitationSystem`, and `CanonicalReference` for identity, plus `MappingAssertion` for equivalence. Locations are recorded as `resolver_targets` entries embedded on each `CanonicalReference`; they are not a separate object type. TextRefs never hosts full text, apparatus, commentary, or copyrighted edition content.
+TextRefs defines a minimal registry standard for stable, machine-addressable references to texts. Its centre is the separation of **identity** from **location**: a reference such as `John.3.16` is one abstract, language-independent identity, while the translations, editions, and providers that carry it are recorded as locations embedded on the reference. `Work` and `CitationSystem` records use flat stable keys; richer bibliographic and authority data is connected through mappings to external systems, with optional lightweight `Work.creators` retained for citation rendering. The model has four registry object types — `Work`, `CitationSystem`, and `CanonicalReference` for identity, plus `MappingAssertion` for curated relations to external identifiers. Locations are recorded as `resolver_targets` entries embedded on each `CanonicalReference`; they are not a separate object type. TextRefs never hosts full text, apparatus, commentary, or copyrighted edition content.
-One identity fans out to many locations and equivalences — adding a translation adds a resolver-target entry to the reference, never a new reference:
+One identity fans out to many locations and mappings — adding a translation adds a resolver-target entry to the reference, never a new reference:
```mermaid
flowchart LR
@@ -19,7 +19,7 @@ flowchart LR
W --> CR
CS --> CR
end
- subgraph equivalence ["Equivalence — same as"]
+ subgraph mapping ["Mapping — same as, or about"]
MA["MappingAssertion CTS URN"]
end
W --> MA
@@ -30,9 +30,9 @@ flowchart LR
- **[Specification](/standard/specification/)** — the normative document: object model, conformance, validation, and the conformance boundary.
- **[Identifier syntax](/standard/identifier-syntax/)** — deterministic UUID v5 generation, namespace, and serialization rules.
- **[Citation-system profiles](/standard/system-profiles/)** — how citation systems constrain locators, with the seed Bekker and Stephanus profiles.
-- **[JSON-LD context](/standard/json-ld/)** — the context mapping TextRefs records onto SKOS, Dublin Core, and schema.org.
+- **[JSON-LD context](/standard/json-ld/)** — the context mapping TextRefs records onto SKOS, PROV-O, Dublin Core, and schema.org.
- **[Versioning & data packaging](/standard/versioning/)** — how the spec and the monthly registry exports are versioned and packaged.
## Cite this spec
-These pages are the working authority for the standard while `v0.1.0-draft` is being settled. The `v1` JSON-LD context is served at `https://textrefs.org/contexts/v1.jsonld`. A frozen, citable release will be tagged once the core stabilises.
+These pages are the working authority for the standard while `v0.1.0-draft` is being settled. The `v1` JSON-LD context is served at `https://textrefs.org/contexts/v1.jsonld`. `v0.1.0` is being prepared as the first citable baseline, cut from this working draft.
diff --git a/src/content/docs/standard/json-ld.md b/src/content/docs/standard/json-ld.md
index 01b2f8a..582b1a3 100644
--- a/src/content/docs/standard/json-ld.md
+++ b/src/content/docs/standard/json-ld.md
@@ -16,14 +16,14 @@ https://textrefs.org/contexts/v1.jsonld
## Vocabularies
-| Prefix | Namespace | Used for |
-| --------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------- |
-| `tr` | `https://textrefs.org/ontology#` | TextRefs object types, keys, and TextRefs-specific metadata |
-| `skos` | `http://www.w3.org/2004/02/skos/core#` | Labels and schemes (`inScheme`) |
-| `dcterms` | `http://purl.org/dc/terms/` | `created`, `modified`, `source`, `language`, `license`, `conformsTo`, `isReferencedBy` mapping relation |
-| `prov` | `http://www.w3.org/ns/prov#` | Work ↔ same-entity mapping relation (`alternateOf`) |
-| `schema` | `https://schema.org/` | `url`, `provider`, `edition`, `creator`, `familyName`, `givenName`, `name` |
-| `xsd` | `http://www.w3.org/2001/XMLSchema#` | `xsd:date` typing for `created` / `modified` / `last_checked` |
+| Prefix | Namespace | Used for |
+| --------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `tr` | `https://textrefs.org/ontology#` | TextRefs object types, keys, and TextRefs-specific metadata |
+| `skos` | `http://www.w3.org/2004/02/skos/core#` | Labels and schemes (`inScheme`) |
+| `dcterms` | `http://purl.org/dc/terms/` | `created`, `modified`, `source`, `language`, `license`, `conformsTo`, `description`, `isReferencedBy` mapping relation, `isReplacedBy` successor link |
+| `prov` | `http://www.w3.org/ns/prov#` | Work ↔ same-entity mapping relation (`alternateOf`) |
+| `schema` | `https://schema.org/` | `url`, `provider`, `edition`, `creator`, `familyName`, `givenName`, `name` |
+| `xsd` | `http://www.w3.org/2001/XMLSchema#` | `xsd:date` typing for `created` / `modified` / `last_checked` |
## Mapping relations
@@ -32,7 +32,7 @@ The mapping relations are chosen by what the target is, never by author confiden
- `alternateOf` → `prov:alternateOf`, when the target is another entity denoting the same work (e.g. a Wikidata item).
- `isReferencedBy` → `dcterms:isReferencedBy`, when the target is a document or page about the work (e.g. a Wikipedia article).
-Published `Work` records additionally carry direct `alternateOf` / `isReferencedBy` arrays derived from their non-tombstoned mapping assertions, published as `prov:alternateOf` / `dcterms:isReferencedBy` edges from the work IRI to the mapped identifiers without dereferencing the reified `MappingAssertion` records. The arrays are a read-only projection; the assertion stays authoritative ([Specification §6](/standard/specification/#6-work)).
+Published `Work` records additionally carry direct `alternateOf` / `isReferencedBy` arrays derived from mapping assertions that are not `withdrawn` or `blocked`, published as `prov:alternateOf` / `dcterms:isReferencedBy` edges from the work IRI to the mapped identifiers without dereferencing the reified `MappingAssertion` records. The arrays are a read-only projection; the assertion stays authoritative ([Specification §6](/standard/specification/#6-work)).
Choose `alternateOf` when the target identifies the same work from a different perspective or at a different level of abstraction; choose `isReferencedBy` when the target describes the work rather than identifying it. See [Specification §10](/standard/specification/#10-mappingassertion).
diff --git a/src/content/docs/standard/specification.md b/src/content/docs/standard/specification.md
index f1f3285..a276b79 100644
--- a/src/content/docs/standard/specification.md
+++ b/src/content/docs/standard/specification.md
@@ -28,7 +28,7 @@ A dataset conforms to the TextRefs Standard if it satisfies all of the following
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 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`.
+7. Every dereferenceable location is represented as an entry in the `resolver_targets` array of its `CanonicalReference`, and every external identifier or cross-reference 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.
@@ -41,7 +41,7 @@ The key words `MUST`, `MUST NOT`, `REQUIRED`, `SHALL`, `SHALL NOT`, `SHOULD`, `S
TextRefs separates **identity** from **location**.
- **Identity** is abstract and language-independent. `Work`, `CitationSystem`, and `CanonicalReference` answer the question "_which_ passage": for example _the New Testament, book-chapter-verse, John.3.16_. There is exactly one such identity, regardless of how many editions, translations, or websites carry it.
-- **Location and equivalence** answer "_where_ can I read it" and "_what else_ is this the same as". The `resolver_targets` array embedded in each `CanonicalReference` lists places where the reference can be read (specific translations, editions, or providers). `MappingAssertion` records that a `Work` is equivalent to an external identifier or to another `Work`.
+- **Location and mapping** answer "_where_ can I read it" and "_what else_ relates to it". The `resolver_targets` array embedded in each `CanonicalReference` lists places where the reference can be read (specific translations, editions, or providers). `MappingAssertion` relates a `Work` to an external identifier or to another `Work` — either as another entity denoting the same work, or as a document about it ([§10](#10-mappingassertion)).
A reference such as `John.3.16` is the **same identity** whether read in Greek, the King James Version, or the Lutherbibel. The translation is a property of the _location_, never of the identity. This is what lets the model scale to works with many editions and translations (see [§13](#13-worked-example-a-multi-translation-work)).
@@ -56,7 +56,7 @@ A conforming registry MUST support these object types. Each top-level object MUS
| `Work` | identity | An abstract textual work. |
| `CitationSystem` | identity | A notation that fragments works into locators. |
| `CanonicalReference` | identity + location | One abstract reference point in a work, with embedded resolver targets. |
-| `MappingAssertion` | equivalence | A curated equivalence between a `Work` and an external identifier. |
+| `MappingAssertion` | relation | A curated relation between a `Work` and an external identifier. |
Dereferenceable locations are not a separate object type. They are recorded as entries in the `resolver_targets` array embedded in each `CanonicalReference` (see [§9](#9-embedded-resolver-targets)). This keeps language-tagged locations co-located with the reference they describe, and means a work with N translations adds N array entries — not N standalone records.
@@ -68,6 +68,7 @@ classDiagram
+URI id
+string key
+string preferred_label
+ +string preferred_citation_system_key
+Creator[] creators
}
class CitationSystem {
@@ -230,14 +231,14 @@ 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 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.
+- `license`, when present, MUST be the canonical SPDX IRI (`https://spdx.org/licenses/{id}`) of a current or deprecated [SPDX license identifier](https://spdx.org/licenses/), so `dcterms:license` has a single IRI-typed range. Authoring formats carry the bare identifier (e.g. `CC0-1.0`, `CC-BY-4.0`) and the published record carries the IRI; see [§14](#14-validation-requirements) item 10. 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.
## 10. MappingAssertion
-A `MappingAssertion` records a curated equivalence claim between a TextRefs `Work` and an **external identifier** (CTS URN, Wikidata Q-ID, DOI, ARK, …) or another TextRefs `Work`. There is no separate object type for external identifiers; they are always expressed as mapping targets.
+A `MappingAssertion` records a curated relation between a TextRefs `Work` and an **external identifier** (CTS URN, Wikidata Q-ID, DOI, ARK, …) or another TextRefs `Work`. There is no separate object type for external identifiers; they are always expressed as mapping targets.
```json
{
@@ -276,7 +277,7 @@ A `MappingAssertion` identifier MUST be generated deterministically from `subjec
The persistence promise attaches at **promotion**: the first time a record is published at status `active` ([§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`, `withdrawn`, or `blocked`, [§12](#12-administrative-metadata)) and SHOULD carry the successor IRI in `superseded_by`. `MappingAssertion`s MUST NOT be used for succession; they are reserved for work-level equivalence ([§10](#10-mappingassertion)).
+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`, `withdrawn`, or `blocked`, [§12](#12-administrative-metadata)) and SHOULD carry the successor IRI in `superseded_by`. `MappingAssertion`s MUST NOT be used for succession; they are reserved for work-level relations to external identifiers ([§10](#10-mappingassertion)). The three tombstone statuses differ in reach: a `deprecated` record is retained and still resolves, while a `withdrawn` or `blocked` record is additionally excluded from `Work` mapping projections ([§6](#6-work)) and MUST NOT be depended on by a live record.
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.
@@ -321,6 +322,7 @@ This is the case that motivates separating identity from location. The New Testa
"key": "new-testament",
"type": "Work",
"preferred_label": "New Testament",
+ "preferred_citation_system_key": "bible-book-chapter-verse",
"status": "active",
"created": "2026-05-31",
"modified": "2026-05-31"
@@ -357,7 +359,7 @@ This is the case that motivates separating identity from location. The New Testa
"edition": "King James Version",
"provider": "Bible Gateway",
"access": "open",
- "license": "CC0-1.0"
+ "license": "https://spdx.org/licenses/CC0-1.0"
}
],
"status": "active",
@@ -385,7 +387,7 @@ A conforming validator MUST check:
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;
-10. `resolver_targets` entries: `access` values, BCP 47 syntax of `language` and its presence for language-specific entries, and SPDX syntax of `license` when present;
+10. `resolver_targets` entries: `access` values, BCP 47 syntax of `language`, and SPDX license IRI syntax of `license` when present ([§9](#9-embedded-resolver-targets)) — the bare SPDX identifier form is an authoring-time check, not a published-record validation concern;
11. mapping `relation` values and the Work-IRI shape of `MappingAssertion.subject`;
12. absence of forbidden full-text/apparatus/commentary content;
13. that no `active` record depends on a `draft` one: an `active` `CanonicalReference` references an `active` `Work` and an `active` `CitationSystem`, and an `active` `MappingAssertion` takes an `active` `Work` as its `subject`; `Work.preferred_citation_system_key` MUST reference a known `CitationSystem`, and an `active` `Work` requires an `active` preferred `CitationSystem`.
@@ -415,7 +417,8 @@ This standard relies on the following external standards. Each is normative wher
| 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) |
| Linked-data serialization | [JSON-LD 1.1](https://www.w3.org/TR/json-ld11/) |
-| Concepts and mapping relations | [SKOS](https://www.w3.org/TR/skos-reference/) |
+| Labels and concept schemes | [SKOS](https://www.w3.org/TR/skos-reference/) |
+| Alternate-presentation relations | [PROV-O](https://www.w3.org/TR/prov-o/) |
| Dates, provenance, language, licence | [Dublin Core Terms](https://www.dublincore.org/specifications/dublin-core/dcmi-terms/) |
| URL, provider, edition, work type | [schema.org](https://schema.org/) |
| Licence identifiers | [SPDX License List](https://spdx.org/licenses/) |
diff --git a/src/content/docs/standard/versioning.md b/src/content/docs/standard/versioning.md
index 622466d..ab569d4 100644
--- a/src/content/docs/standard/versioning.md
+++ b/src/content/docs/standard/versioning.md
@@ -14,6 +14,8 @@ TextRefs versions three things that move at different speeds, and archives each
| Registry data | `textrefs/registry` | `vYYYY.MM.N` | TextRefs Registry |
| Data-package version | inside `datapackage.json` | SemVer **without** leading `v` | (carried within registry deposit) |
+The release tag and the spec document's own version string (e.g. `0.1.0-draft` in `specification.md`) are independent labels, not aliases: the tag marks the site repository's release, the document version marks the spec's own maturity, and the two are not required to read identically.
+
The site repository couples the spec, JSON-LD context, Zod schemas, and Astro site under a single tag because pre-1.0 the site is the spec's reference rendering; splitting them now would create empty changelogs and confuse Zenodo metadata. Registry data is decoupled — record changes flow on their own cadence — and lives in a separate repository because the [Zenodo–GitHub integration](https://docs.github.com/en/repositories/archiving-a-github-repository/referencing-and-citing-content) mints one concept DOI per repository. The two repositories are cross-linked via `.zenodo.json` `related_identifiers`.
The site repository includes `textrefs/registry` as a git submodule at `data/`. The registry uses `main` as its working branch. The site pins a specific registry `main` commit through the submodule pointer, and its compiler builds registry dumps from that pinned content for reproducible site releases.
@@ -31,7 +33,7 @@ Each `/standard/*` page carries a `maturity` field in its frontmatter, encoding
Transitions:
-- `0.x` releases stay `working-draft` regardless of any `-draft` suffix on the tag.
+- `0.x` releases stay `working-draft` regardless of any `-draft` suffix on the spec document's version string.
- First `1.0.0-rc.1` enters `candidate-recommendation`.
- `1.0.0` enters `recommendation`.
@@ -87,7 +89,7 @@ Registry identity is permanent once promoted: the IRI of a `Work`, `CitationSyst
### Schema
-Tombstones use a status value plus one optional field. The old record stays in the data tree with `status: withdrawn` (or `blocked`, for a rights or policy dispute). If a successor exists, the record carries its IRI in `superseded_by` (`dcterms:isReplacedBy` in the published context); a `deprecated` record may do the same. 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.
+Tombstones use a status value plus one optional field. The old record stays in the data tree with `status: withdrawn` (or `blocked`, for a rights or policy dispute). If a successor exists, the record carries its IRI in `superseded_by` (`dcterms:isReplacedBy` in the published context); a `deprecated` record may do the same. Consumers follow `superseded_by` to find the successor. `MappingAssertion`s are reserved for work-level relations to external identifiers and MUST NOT be used for succession links.
### On-disk representation
@@ -112,7 +114,7 @@ The compiler enforces these invariants, and fails the build on any violation:
5. An `active` `CanonicalReference` MUST have an `active` `Work` and an `active` `CitationSystem` for its own `citation_system_key`. Works and systems are promoted to `active` before or together with whatever depends on them.
6. An `active` `MappingAssertion` MUST take an `active` `Work` as its `subject`.
-Two further checks run when a source file is parsed, before any record is built: a work MUST NOT declare the same citation system twice, and a `locator` MUST NOT contain `/` — the alias grammar below distinguishes its two forms by segment count alone.
+Two further checks apply. A work MUST NOT declare the same citation system twice, checked when a source file is parsed, before any record is built. A `locator` MUST NOT contain `/`, checked per reference during emission — the alias grammar below distinguishes its two forms by segment count alone.
### Aliases vs. tombstones
diff --git a/standard/schema/canonical-reference.ts b/standard/schema/canonical-reference.ts
index 9bc8f54..f3356a7 100644
--- a/standard/schema/canonical-reference.ts
+++ b/standard/schema/canonical-reference.ts
@@ -1,9 +1,9 @@
import { z } from 'zod';
-import { AdminMetadata, FlatKey, Iri, IsoDate } from './common.js';
+import { AdminMetadata, FlatKey, Iri, IsoDate, LanguageTag } from './common.js';
export const ResolverTargetEntry = z.object({
url: Iri,
- language: z.string().min(2).optional(),
+ language: LanguageTag.optional(),
edition: z.string().optional(),
provider: z.string().optional(),
access: z.enum(['open', 'paywalled', 'restricted', 'unknown']),
diff --git a/standard/schema/common.ts b/standard/schema/common.ts
index 88114f1..893fa92 100644
--- a/standard/schema/common.ts
+++ b/standard/schema/common.ts
@@ -30,3 +30,17 @@ 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._-]*$');
+
+// BCP 47 well-formedness (RFC 5646 langtag + privateuse). Registry validity is
+// not checked — only that the tag parses. Grandfathered tags split on that same
+// line: the nine regular ones (art-lojban, zh-min-nan, …) are well-formed
+// langtags and pass, and the seventeen irregular ones are out of scope — both
+// the i- forms (i-klingon, …) and the four without that prefix (en-GB-oed,
+// sgn-BE-FR, sgn-BE-NL, sgn-CH-DE). No text language in the registry needs one.
+// `scripts/compile.test.ts` locks this split to the regex.
+export const LanguageTag = z
+ .string()
+ .regex(
+ /^(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}){0,3}|[A-Za-z]{4,8})(?:-[A-Za-z]{4})?(?:-(?:[A-Za-z]{2}|\d{3}))?(?:-(?:[\dA-Za-z]{5,8}|\d[\dA-Za-z]{3}))*(?:-[\dA-WY-Za-wy-z](?:-[\dA-Za-z]{2,8})+)*(?:-[Xx](?:-[\dA-Za-z]{1,8})+)?$|^[Xx](?:-[\dA-Za-z]{1,8})+$/,
+ 'must be a well-formed BCP 47 language tag',
+ );
diff --git a/standard/schema/work.ts b/standard/schema/work.ts
index 3517ad5..656fa57 100644
--- a/standard/schema/work.ts
+++ b/standard/schema/work.ts
@@ -27,8 +27,8 @@ export const WorkBase = AdminMetadata.extend({
// known CitationSystem.
preferred_citation_system_key: FlatKey,
creators: z.array(Creator).optional(),
- // Compiler-derived projection of the work's non-tombstoned
- // MappingAssertions (prov:alternateOf / dcterms:isReferencedBy in the
+ // Compiler-derived projection of the work's MappingAssertions that are not
+ // withdrawn or blocked (prov:alternateOf / dcterms:isReferencedBy in the
// published context, ADR-0006). Never authored.
alternateOf: z.array(Iri).optional(),
isReferencedBy: z.array(Iri).optional(),
From f5a037f8629e292ea5f49cf4bd7355193fafcaf1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
Date: Mon, 24 Aug 2026 17:42:23 +0200
Subject: [PATCH 41/45] fix(compile): stop projecting retired mapping
assertions onto Work (#45) (#86)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The direct `alternateOf` / `isReferencedBy` edges on `Work` excluded only
`withdrawn` and `blocked` assertions. `deprecated` still projected, so a
mapping the registry had taken out of use kept emitting a live edge.
That contradicts the standard's own definitions. `deprecated`, `withdrawn`
and `blocked` are the three states for records that have left active use
(ADR-0004, `standard/schema/common.ts`), and specification §12 defines
`deprecated` as "retained but no longer recommended". The edge carries no
status, so it cannot say the mapping is retired.
Gate the projection on the set that already means "left active use", and
rename it from SUPERSEDABLE_STATUSES to RETIRED_STATUSES so it reads
correctly at both call sites. TOMBSTONE_STATUSES stays: the two remaining
uses are about broken resolution, and a deprecated record still resolves.
The published output does not change. Every record in `data/` is `draft`,
so no assertion is deprecated today and `dist/dump/works.jsonl` is
byte-identical. This closes the hole before a demotion ever happens.
Issue #45 also proposed an allowlist, so that `draft` assertions stop
projecting too. Declined: TextRefs' guarantees attach to
`CanonicalReference`, and the Work projection is enrichment that never
claimed review. The docs now say so, rather than implying a promise the
standard does not make.
Claude-Session: https://claude.ai/code/session_017PUm3GZBhwznL6uZEpDMKg
Co-authored-by: Moritz Mähr <14755525+maehr@users.noreply.github.com>
Co-authored-by: Claude Opus 5
---
api/openapi.yaml | 9 ++++--
scripts/compile.test.ts | 35 ++++++++++++++++++++++
scripts/compile.ts | 10 +++++--
src/content/docs/standard/json-ld.md | 2 +-
src/content/docs/standard/specification.md | 2 +-
standard/schema/work.ts | 5 ++--
6 files changed, 53 insertions(+), 10 deletions(-)
diff --git a/api/openapi.yaml b/api/openapi.yaml
index 816b02a..85a8cf0 100644
--- a/api/openapi.yaml
+++ b/api/openapi.yaml
@@ -243,13 +243,16 @@ components:
items: { $ref: '#/components/schemas/Iri' }
description:
Compiler-derived from the work's alternateOf MappingAssertions
- that are not withdrawn or blocked. Read-only, never authored.
+ that are not deprecated, withdrawn or blocked. The array enriches
+ the work and makes no review claim. Read the MappingAssertion for
+ status. Read-only, never authored.
isReferencedBy:
type: array
items: { $ref: '#/components/schemas/Iri' }
description: Compiler-derived from the work's isReferencedBy
- MappingAssertions that are not withdrawn or blocked.
- Read-only, never authored.
+ MappingAssertions that are not deprecated, withdrawn or blocked.
+ The array enriches the work and makes no review claim. Read the
+ MappingAssertion for status. Read-only, never authored.
CitationSystem:
allOf:
- $ref: '#/components/schemas/AdminMetadata'
diff --git a/scripts/compile.test.ts b/scripts/compile.test.ts
index dc453d4..bfd2ded 100644
--- a/scripts/compile.test.ts
+++ b/scripts/compile.test.ts
@@ -332,6 +332,41 @@ mappings:
assert.equal(reg.mappings.length, 2);
});
+test('deprecated mappings are excluded from the projection', () => {
+ const reg = compileFixture({
+ systems: twoSystems,
+ works: {
+ 'test.work': `${workHeader()}
+citation_system: primary-section
+references:
+ - '5'
+
+mappings:
+ - relation: alternateOf
+ identifier: 'https://www.wikidata.org/entity/Q1'
+ source: manual-curation
+ status: deprecated
+ created: 2026-01-01
+ modified: 2026-01-01
+ - relation: isReferencedBy
+ identifier: 'https://en.wikipedia.org/wiki/Test'
+ source: manual-curation
+ status: deprecated
+ created: 2026-01-01
+ modified: 2026-01-01
+`,
+ },
+ });
+ const work = reg.works.find((w) => w.key === 'test.work');
+ assert.ok(work);
+ // `deprecated` is retained but no longer recommended (specification §12), so
+ // the unqualified edge must not advertise it (#45).
+ assert.equal(work.alternateOf, undefined);
+ assert.equal(work.isReferencedBy, undefined);
+ // The reified assertions survive; only the projection drops them.
+ assert.equal(reg.mappings.length, 2);
+});
+
test('mapping IRIs are deterministic from [subject, relation, target]', () => {
const reg = compileFixture(workWithMappings());
// Recomputed independently here, exactly as validate-data.ts and any
diff --git a/scripts/compile.ts b/scripts/compile.ts
index d7b95a1..d0562ac 100644
--- a/scripts/compile.ts
+++ b/scripts/compile.ts
@@ -483,12 +483,14 @@ export function compileRegistry(dataRootOverride?: string): CompiledRegistry {
// the context, ADR-0006) projected from the work's mapping assertions,
// in addition to the reified MappingAssertion records below. Keyed off
// the relation enum so adding a relation needs no branch here.
+ // A retired assertion does not project: the edge carries no status, so
+ // it would advertise a mapping the registry has taken out of use (#45).
const mappingEdges: Record = {
alternateOf: [],
isReferencedBy: [],
};
for (const m of src.mappings ?? []) {
- if (TOMBSTONE_STATUSES.has(m.status)) continue;
+ if (RETIRED_STATUSES.has(m.status)) continue;
mappingEdges[m.relation].push(m.identifier);
}
@@ -631,7 +633,9 @@ type StatusRecord = {
};
const TOMBSTONE_STATUSES = new Set(['withdrawn', 'blocked']);
-const SUPERSEDABLE_STATUSES = new Set(['deprecated', 'withdrawn', 'blocked']);
+// ADR-0004: deprecated, withdrawn and blocked are the states for records that
+// have left active use.
+const RETIRED_STATUSES = new Set(['deprecated', 'withdrawn', 'blocked']);
function enforceRegistryInvariants(reg: {
works: Work[];
@@ -656,7 +660,7 @@ function enforceRegistryInvariants(reg: {
// superseded_by carries the successor of a record that has left active use.
// A record still in use has none.
for (const r of all) {
- if (r.superseded_by !== undefined && !SUPERSEDABLE_STATUSES.has(r.status)) {
+ if (r.superseded_by !== undefined && !RETIRED_STATUSES.has(r.status)) {
errors.push(
`${r.id}: superseded_by is only allowed on deprecated/withdrawn/blocked records (status: ${r.status})`,
);
diff --git a/src/content/docs/standard/json-ld.md b/src/content/docs/standard/json-ld.md
index 582b1a3..31bb0c7 100644
--- a/src/content/docs/standard/json-ld.md
+++ b/src/content/docs/standard/json-ld.md
@@ -32,7 +32,7 @@ The mapping relations are chosen by what the target is, never by author confiden
- `alternateOf` → `prov:alternateOf`, when the target is another entity denoting the same work (e.g. a Wikidata item).
- `isReferencedBy` → `dcterms:isReferencedBy`, when the target is a document or page about the work (e.g. a Wikipedia article).
-Published `Work` records additionally carry direct `alternateOf` / `isReferencedBy` arrays derived from mapping assertions that are not `withdrawn` or `blocked`, published as `prov:alternateOf` / `dcterms:isReferencedBy` edges from the work IRI to the mapped identifiers without dereferencing the reified `MappingAssertion` records. The arrays are a read-only projection; the assertion stays authoritative ([Specification §6](/standard/specification/#6-work)).
+Published `Work` records additionally carry direct `alternateOf` / `isReferencedBy` arrays derived from mapping assertions that are not `deprecated`, `withdrawn` or `blocked`, published as `prov:alternateOf` / `dcterms:isReferencedBy` edges from the work IRI to the mapped identifiers without dereferencing the reified `MappingAssertion` records. The arrays are a read-only projection; the assertion stays authoritative ([Specification §6](/standard/specification/#6-work)). The arrays enrich the work. They make no claim about review: a consumer that needs the status of a mapping must read the `MappingAssertion`.
Choose `alternateOf` when the target identifies the same work from a different perspective or at a different level of abstraction; choose `isReferencedBy` when the target describes the work rather than identifying it. See [Specification §10](/standard/specification/#10-mappingassertion).
diff --git a/src/content/docs/standard/specification.md b/src/content/docs/standard/specification.md
index a276b79..ac7cb8c 100644
--- a/src/content/docs/standard/specification.md
+++ b/src/content/docs/standard/specification.md
@@ -138,7 +138,7 @@ Required: `id`, `key`, `type` (`Work`), `preferred_label`, `preferred_citation_s
External identifiers for a `Work` (e.g. Wikidata Q-ID, DOI, VIAF) are asserted as `MappingAssertion`s whose `subject` is the `Work` ([§10](#10-mappingassertion)). They MUST NOT be authored directly on the `Work`.
-`alternateOf` and `isReferencedBy` are the compiler's read-only projection of those assertions — every one whose `status` is not `withdrawn` or `blocked`, grouped by `relation` — published straight from the work IRI as `prov:alternateOf` / `dcterms:isReferencedBy` edges ([JSON-LD](/standard/json-ld/#mapping-relations)). They carry no status or provenance; the `MappingAssertion` stays authoritative.
+`alternateOf` and `isReferencedBy` are the compiler's read-only projection of those assertions — every one whose `status` is not `deprecated`, `withdrawn` or `blocked`, grouped by `relation` — published straight from the work IRI as `prov:alternateOf` / `dcterms:isReferencedBy` edges ([JSON-LD](/standard/json-ld/#mapping-relations)). They carry no status or provenance; the `MappingAssertion` stays authoritative. The arrays enrich the work. They make no claim about review: a consumer that needs the status of a mapping MUST read the `MappingAssertion`.
## 7. CitationSystem
diff --git a/standard/schema/work.ts b/standard/schema/work.ts
index 656fa57..0d64af1 100644
--- a/standard/schema/work.ts
+++ b/standard/schema/work.ts
@@ -28,8 +28,9 @@ export const WorkBase = AdminMetadata.extend({
preferred_citation_system_key: FlatKey,
creators: z.array(Creator).optional(),
// Compiler-derived projection of the work's MappingAssertions that are not
- // withdrawn or blocked (prov:alternateOf / dcterms:isReferencedBy in the
- // published context, ADR-0006). Never authored.
+ // deprecated, withdrawn or blocked (prov:alternateOf / dcterms:isReferencedBy
+ // in the published context, ADR-0006). The arrays enrich the work; they make
+ // no review claim. Read the MappingAssertion for status. Never authored.
alternateOf: z.array(Iri).optional(),
isReferencedBy: z.array(Iri).optional(),
});
From a4a3b85e026d0170b2fbab3b636dbd7452819b06 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
Date: Mon, 24 Aug 2026 20:54:28 +0200
Subject: [PATCH 42/45] feat(api): add works and systems collection endpoints
(#83) (#87)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Every operation in the v0.1.0 contract read one record by its own identifier, so
a client had to know a key before it could fetch anything. Registry discovery was
a build-time privilege: only the `/reg/` browser could call `loadWorks()` and
`loadSystems()`.
Add two static JSON-LD collections at `/reg/works.json` and `/reg/systems.json`.
Each body carries one `@context` and one `@graph`, sorted by key, in every status.
Items are the records that `/id/…json` serves, without a per-item `@context`.
- Add `src/lib/collection.ts` with the shared sort order, so the HTML browser and
the JSON collections cannot drift. Covered by `src/lib/collection.test.ts`.
- Advertise both collections from `/reg/` with `link rel="alternate"`. Each link
carries a `title`, because the page also emits `hreflang` alternates.
- Add `Collections` and `Bulk` tags, both collection paths, the five `/dump/`
paths that the compiler already writes, and the `WorkCollection` /
`SystemCollection` schemas.
Declare the media types the host actually serves. The site is static, and GitHub
Pages derives every `Content-Type` from the file extension and drops the header
the Astro `APIRoute` sets. A `.json` body arrives as `application/json` and a
`.jsonl` body as `application/octet-stream`, whatever the payload is. The
contract now says so, and tells clients to parse by documented shape rather than
by response header.
`RegistryObject` stays unreferenced. It mirrors the exported Zod union in
`standard/schema/index.ts` and is schema parity, not debris.
Closes #83
---
api/openapi.yaml | 140 +++++++++++++++++++++
src/content/docs/get-started/url-layout.md | 43 ++++++-
src/lib/collection.test.ts | 38 ++++++
src/lib/collection.ts | 15 +++
src/pages/reg/index.astro | 25 +++-
src/pages/reg/systems.json.ts | 8 ++
src/pages/reg/works.json.ts | 8 ++
7 files changed, 274 insertions(+), 3 deletions(-)
create mode 100644 src/lib/collection.test.ts
create mode 100644 src/lib/collection.ts
create mode 100644 src/pages/reg/systems.json.ts
create mode 100644 src/pages/reg/works.json.ts
diff --git a/api/openapi.yaml b/api/openapi.yaml
index 85a8cf0..bea5b19 100644
--- a/api/openapi.yaml
+++ b/api/openapi.yaml
@@ -21,6 +21,18 @@ info:
The `Accept-Language` / `edition`-based 303 redirect on
`/id/ref/{uuid}` to a resolver-target manifestation is planned but not
yet live.
+
+ A client that does not yet know a key starts at `/reg/works.json` or
+ `/reg/systems.json`. `/dump/` carries the whole registry as
+ newline-delimited JSON for bulk consumers.
+
+ The site is static, and the host derives every `Content-Type` from the
+ file extension. This contract therefore declares the media type that
+ the host actually sends, not the media type that best describes the
+ payload. A `.json` body is JSON-LD by content and arrives as
+ `application/json`. A `.jsonl` body is newline-delimited JSON by
+ content and arrives as `application/octet-stream`. Parse by
+ documented shape, not by response header.
license:
name: AGPL-3.0-or-later
url: https://www.gnu.org/licenses/agpl-3.0.html
@@ -33,6 +45,15 @@ servers:
tags:
- name: Records
description: Canonical record URLs under `/id/`.
+ - name: Collections
+ description:
+ Registry-wide JSON-LD collections under `/reg/`. Entry points for a
+ client that does not yet know a key.
+ - name: Bulk
+ description:
+ Whole-registry artifacts under `/dump/`, rebuilt on every deploy. The
+ host serves them as `application/octet-stream`; the bodies are
+ newline-delimited JSON.
paths:
/id/work/{key}/:
get:
@@ -146,6 +167,105 @@ paths:
application/json:
schema: { $ref: '#/components/schemas/MappingAssertion' }
'404': { $ref: '#/components/responses/NotFound' }
+ /reg/works.json:
+ get:
+ tags: [Collections]
+ summary: Work collection — JSON-LD
+ operationId: getWorkCollection
+ description:
+ Each item in `@graph` is the same record that `/id/work/{key}.json`
+ serves, without its own `@context`. The collection carries one
+ `@context` for the whole graph.
+ responses:
+ '200':
+ description:
+ Every Work record in the registry, in every status, sorted by
+ key.
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/WorkCollection' }
+ /reg/systems.json:
+ get:
+ tags: [Collections]
+ summary: CitationSystem collection — JSON-LD
+ operationId: getCitationSystemCollection
+ description: Each item in `@graph` is the same record that
+ `/id/system/{key}.json` serves, without its own `@context`. The
+ collection carries one `@context` for the whole graph.
+ responses:
+ '200':
+ description:
+ Every CitationSystem record in the registry, in every status,
+ sorted by key.
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/SystemCollection' }
+ /dump/works.jsonl:
+ get:
+ tags: [Bulk]
+ summary: Work dump — NDJSON
+ operationId: getWorksDump
+ responses:
+ '200':
+ description:
+ Newline-delimited JSON, one Work record object per line, with
+ no `@context`.
+ content:
+ application/octet-stream:
+ schema: { type: string }
+ /dump/citation-systems.jsonl:
+ get:
+ tags: [Bulk]
+ summary: CitationSystem dump — NDJSON
+ operationId: getCitationSystemsDump
+ responses:
+ '200':
+ description:
+ Newline-delimited JSON, one CitationSystem record object per
+ line, with no `@context`.
+ content:
+ application/octet-stream:
+ schema: { type: string }
+ /dump/references.jsonl:
+ get:
+ tags: [Bulk]
+ summary: CanonicalReference dump — NDJSON
+ operationId: getReferencesDump
+ responses:
+ '200':
+ description:
+ Newline-delimited JSON, one CanonicalReference record object
+ per line, with no `@context`.
+ content:
+ application/octet-stream:
+ schema: { type: string }
+ /dump/mappings.jsonl:
+ get:
+ tags: [Bulk]
+ summary: MappingAssertion dump — NDJSON
+ operationId: getMappingsDump
+ responses:
+ '200':
+ description:
+ Newline-delimited JSON, one MappingAssertion record object per
+ line, with no `@context`.
+ content:
+ application/octet-stream:
+ schema: { type: string }
+ /dump/datapackage.json:
+ get:
+ tags: [Bulk]
+ summary: Frictionless data-package descriptor
+ operationId: getDataPackage
+ responses:
+ '200':
+ description:
+ The Frictionless data-package descriptor that lists the four
+ JSONL resources with, for each, its byte count and its
+ `sha256:` hash, plus the registry version.
+ content:
+ application/json:
+ schema: { type: object }
components:
parameters:
FlatKey:
@@ -341,6 +461,26 @@ components:
items: { $ref: '#/components/schemas/Iri' }
minItems: 1
source: { type: string, minLength: 1 }
+ WorkCollection:
+ description: The `/reg/works.json` registry-wide collection of Work records.
+ type: object
+ required: ['@context', '@graph']
+ properties:
+ '@context': { type: string, format: uri }
+ '@graph':
+ type: array
+ items: { $ref: '#/components/schemas/Work' }
+ SystemCollection:
+ description:
+ The `/reg/systems.json` registry-wide collection of CitationSystem
+ records.
+ type: object
+ required: ['@context', '@graph']
+ properties:
+ '@context': { type: string, format: uri }
+ '@graph':
+ type: array
+ items: { $ref: '#/components/schemas/CitationSystem' }
RegistryObject:
oneOf:
- { $ref: '#/components/schemas/Work' }
diff --git a/src/content/docs/get-started/url-layout.md b/src/content/docs/get-started/url-layout.md
index 835a04a..844948a 100644
--- a/src/content/docs/get-started/url-layout.md
+++ b/src/content/docs/get-started/url-layout.md
@@ -10,7 +10,7 @@ TextRefs uses four URL prefixes, each with one job. Together they make every reg
| Prefix | Role | What lives there |
| -------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `/id/` | **Identifier** | The canonical, persistent URL of every record. Each record is published twice: `/id/.../` (HTML) and a sibling `/id/....json` (JSON-LD). |
-| `/reg/` | **Browse** | The human registry browser: filter works and citation systems, then browse paginated reference lists from work pages. Links into `/id/`. |
+| `/reg/` | **Browse** | The human registry browser: filter works and citation systems, then browse paginated reference lists from work pages. Links into `/id/`. Also serves two JSON-LD collections, so a client can discover records without a key. |
| `/cite/` | **Cite** | Short, memorable URLs (`/cite/{work}/{system}/{locator}` always, and bare `/cite/{work}/{locator}` for the work's preferred system) that redirect to the canonical `/id/` URL. Convenience only. Bare aliases MAY be retargeted. `/id/` is permanent. |
| `/api/` | **API docs** | The OpenAPI document that describes the `/id/` URL contract, plus the JSON-LD `@context` at `/contexts/`. |
@@ -49,6 +49,47 @@ A client either reads that `` tag, or appends `.json` to the canonical URL
This mirrors how arxiv.org publishes each paper at `/abs/{id}` and `/pdf/{id}` — two static URLs, two representations, no negotiation needed.
+## Collections and bulk data
+
+Every `/id/` URL needs a key you already know. The `/reg/` collections give a client a starting point when it does not have one yet.
+
+`/reg/` serves two static JSON-LD collections:
+
+- `https://textrefs.org/reg/works.json` — every work in the registry
+- `https://textrefs.org/reg/systems.json` — every citation system in the registry
+
+Each collection wraps its records in one `@context` and one `@graph` array:
+
+```json
+{
+ "@context": "https://textrefs.org/contexts/v1.jsonld",
+ "@graph": [
+ {
+ "id": "https://textrefs.org/id/work/plato.republic",
+ "key": "plato.republic",
+ "type": "Work",
+ "…": "…"
+ }
+ ]
+}
+```
+
+Each item carries no `@context` of its own. The collection lists records of every status — active, draft, and retired — sorted by `key`.
+
+For bulk use, five artifacts live under `/dump/`:
+
+- `https://textrefs.org/dump/works.jsonl`
+- `https://textrefs.org/dump/citation-systems.jsonl`
+- `https://textrefs.org/dump/references.jsonl`
+- `https://textrefs.org/dump/mappings.jsonl`
+- `https://textrefs.org/dump/datapackage.json` — a Frictionless data-package descriptor. It lists the four JSONL resources with a byte count and a `sha256:` hash for each.
+
+Each `.jsonl` file holds one record per line, with no `@context`. This is the bulk-archive form of the registry.
+
+The site is static, and the host sets each `Content-Type` from the file extension. A `.json` body is JSON-LD by content, but it arrives as `application/json`. A `.jsonl` body arrives as `application/octet-stream`. Parse each body by its documented shape. Do not parse it by the response header.
+
+Use the small JSON-LD collection for a browser client. Use the `/dump/` files for a bulk consumer. See [`/api/`](/api/) for the full contract.
+
## Why four prefixes, not one
Persistent-identifier systems separate concerns. DOI and ORCID each have a canonical resolver URL that _is_ the API. Their documentation lives at a stable but distinct path. W3ID and PURL add short-alias redirects on top. TextRefs follows the same pattern:
diff --git a/src/lib/collection.test.ts b/src/lib/collection.test.ts
new file mode 100644
index 0000000..dc7bbb7
--- /dev/null
+++ b/src/lib/collection.test.ts
@@ -0,0 +1,38 @@
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { collectionBody, JSONLD_CONTEXT } from './collection.js';
+import { fixtureRegistry } from './registry.fixture.js';
+
+test('collectionBody sets the shared JSON-LD context', () => {
+ const body = collectionBody(fixtureRegistry.systems);
+ assert.equal(body['@context'], 'https://textrefs.org/contexts/v1.jsonld');
+ assert.equal(body['@context'], JSONLD_CONTEXT);
+});
+
+test('collectionBody sorts @graph by key', () => {
+ const unsorted = [...fixtureRegistry.systems].reverse();
+ assert.ok(unsorted.length > 1);
+ const body = collectionBody(unsorted);
+ const keys = body['@graph'].map((r) => r.key);
+ const expected = [...keys].sort((a, b) => a.localeCompare(b));
+ assert.deepEqual(keys, expected);
+});
+
+test('collectionBody does not mutate the input array', () => {
+ const unsorted = [...fixtureRegistry.systems].reverse();
+ const before = unsorted.map((r) => r.key);
+ collectionBody(unsorted);
+ assert.deepEqual(
+ unsorted.map((r) => r.key),
+ before,
+ );
+});
+
+test('collectionBody passes items through unchanged with no per-item @context', () => {
+ const body = collectionBody(fixtureRegistry.systems);
+ const item = body['@graph'].find(
+ (r) => r.key === fixtureRegistry.systems[0].key,
+ );
+ assert.deepEqual(item, fixtureRegistry.systems[0]);
+ assert.equal(item && '@context' in item, false);
+});
diff --git a/src/lib/collection.ts b/src/lib/collection.ts
new file mode 100644
index 0000000..1a10f05
--- /dev/null
+++ b/src/lib/collection.ts
@@ -0,0 +1,15 @@
+// Registry-wide JSON-LD collections served under `/reg/` (works and citation
+// systems). The sort order here is shared with the HTML registry browser
+// (`src/pages/reg/index.astro`), so the two views cannot drift apart.
+
+export const JSONLD_CONTEXT = 'https://textrefs.org/contexts/v1.jsonld';
+
+export function byKey(a: { key: string }, b: { key: string }): number {
+ return a.key.localeCompare(b.key);
+}
+
+export function collectionBody(
+ records: readonly T[],
+) {
+ return { '@context': JSONLD_CONTEXT, '@graph': [...records].sort(byKey) };
+}
diff --git a/src/pages/reg/index.astro b/src/pages/reg/index.astro
index 9eca75d..936811b 100644
--- a/src/pages/reg/index.astro
+++ b/src/pages/reg/index.astro
@@ -2,9 +2,10 @@
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
import { loadWorks, loadSystems } from '../../lib/registry.ts';
import { UNSTABLE_BANNER } from '../../lib/banner.ts';
+import { byKey } from '../../lib/collection.ts';
-const works = [...loadWorks()].sort((a, b) => a.key.localeCompare(b.key));
-const systems = [...loadSystems()].sort((a, b) => a.key.localeCompare(b.key));
+const works = [...loadWorks()].sort(byKey);
+const systems = [...loadSystems()].sort(byKey);
---
a.key.localeCompare(b.key));
template: 'doc',
tableOfContents: false,
banner: UNSTABLE_BANNER,
+ head: [
+ {
+ tag: 'link',
+ attrs: {
+ rel: 'alternate',
+ type: 'application/ld+json',
+ href: '/reg/works.json',
+ title: 'Works',
+ },
+ },
+ {
+ tag: 'link',
+ attrs: {
+ rel: 'alternate',
+ type: 'application/ld+json',
+ href: '/reg/systems.json',
+ title: 'Citation systems',
+ },
+ },
+ ],
}}
>
diff --git a/src/pages/reg/systems.json.ts b/src/pages/reg/systems.json.ts
new file mode 100644
index 0000000..592a28b
--- /dev/null
+++ b/src/pages/reg/systems.json.ts
@@ -0,0 +1,8 @@
+import type { APIRoute } from 'astro';
+import { loadSystems } from '../../lib/registry.ts';
+import { collectionBody } from '../../lib/collection.ts';
+
+export const GET: APIRoute = () =>
+ new Response(JSON.stringify(collectionBody(loadSystems()), null, 2), {
+ headers: { 'Content-Type': 'application/ld+json; charset=utf-8' },
+ });
diff --git a/src/pages/reg/works.json.ts b/src/pages/reg/works.json.ts
new file mode 100644
index 0000000..c3226f6
--- /dev/null
+++ b/src/pages/reg/works.json.ts
@@ -0,0 +1,8 @@
+import type { APIRoute } from 'astro';
+import { loadWorks } from '../../lib/registry.ts';
+import { collectionBody } from '../../lib/collection.ts';
+
+export const GET: APIRoute = () =>
+ new Response(JSON.stringify(collectionBody(loadWorks()), null, 2), {
+ headers: { 'Content-Type': 'application/ld+json; charset=utf-8' },
+ });
From 8bcfdcaf6cd743e8c50bb1e62a39a8a4ce3eca85 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
Date: Mon, 24 Aug 2026 21:34:31 +0200
Subject: [PATCH 43/45] feat(api): resolve a reference by work and locator
without its UUID (#84) (#88)
Publish the reference lookup as data. Each work gets a locator index at
/reg/work/{key}/aliases.json that maps a locator to its reference UUID,
grouped by citation system key, so a client resolves a passage in two
fetches and needs no UUIDv5 implementation of its own.
Publish the complete alias table at /dump/aliases.json, and register it in
datapackage.json with a byte count and a sha256 hash. data/README.md and
data/.gitignore both named that artifact already; no code wrote it.
Split writeDump() into pure halves so a plain JSON resource fits beside the
four JSONL ones. Sort the alias keys, so the body and its hash depend on
the registry content alone. Add the new file to the release workflow, which
sets fail_on_unmatched_files.
Correct the four contract statements that the new artifacts falsify: the
Collections tag, the Bulk tag, the datapackage resource count, and the
claim that every .json body is JSON-LD by content.
Largest emitted index is tanakh at 1.12 MB, under the agreed 2 MB cap, so
no pagination. All 67,959 references are reachable from their work index,
and the four JSONL bodies are byte-identical to staging.
---
.github/workflows/release.yml | 1 +
AGENTS.md | 2 +-
README.md | 26 ++--
api/openapi.yaml | 112 +++++++++++++--
scripts/compile.test.ts | 105 ++++++++++++++-
scripts/compile.ts | 138 +++++++++++++------
src/content/docs/get-started/authoring.md | 6 +-
src/content/docs/get-started/url-layout.md | 60 +++++++--
src/content/docs/standard/versioning.md | 5 +-
src/lib/alias-index.test.ts | 150 +++++++++++++++++++++
src/lib/alias-index.ts | 96 +++++++++++++
src/pages/id/work/[key]/index.astro | 16 +++
src/pages/reg/work/[key]/aliases.json.ts | 25 ++++
13 files changed, 658 insertions(+), 84 deletions(-)
create mode 100644 src/lib/alias-index.test.ts
create mode 100644 src/lib/alias-index.ts
create mode 100644 src/pages/reg/work/[key]/aliases.json.ts
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index bf481aa..c22f383 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -34,6 +34,7 @@ jobs:
with:
files: |
dist/dump/*.jsonl
+ dist/dump/aliases.json
dist/dump/datapackage.json
fail_on_unmatched_files: true
generate_release_notes: true
diff --git a/AGENTS.md b/AGENTS.md
index 07fea0b..987f74d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -24,7 +24,7 @@ Husky installs on `npm install`: Prettier runs on staged files, `commitlint` enf
- `src/content/docs/{association,standard,api,community,get-started}/` — site sections (autogenerated sidebar)
- `src/content/docs/de/` — German mirror (see Localization)
- `src/pages/id/{work,system,ref,mapping}/` — canonical record pages (`/id/.../` HTML and `.json` JSON-LD siblings)
-- `src/pages/reg/` — registry browser (index + paginated reference lists); does not host individual records
+- `src/pages/reg/` — registry browser (index + paginated reference lists), the registry-wide JSON-LD collections, and the per-work locator index (`/reg/work/{key}/aliases.json`); does not host individual records
- `src/pages/cite/` — short-alias redirects (`/cite/{work}/{system}/{locator}` always; bare `/cite/{work}/{locator}` for a work's preferred citation system, and retargetable if that preference changes) → canonical `/id/` URL
- `src/styles/brand.css` — brand tokens
- `public/{logo.svg,favicon.svg,fonts/}` — brand assets
diff --git a/README.md b/README.md
index bce8fb7..143680a 100644
--- a/README.md
+++ b/README.md
@@ -71,19 +71,19 @@ Prerequisites: Node 24 and npm.
Configuration lives in `.env`; use [`.env.example`](./.env.example) as the starting point. `SITE_DOMAIN` controls Astro's canonical `site` URL and defaults to `textrefs.org` when unset.
-| Command | Action |
-| :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| `npm install` | Install dependencies; wires git hooks (husky + lint-staged) |
-| `npm run dev` | Start local dev server at `localhost:4321` |
-| `npm run build` | Build the production site to `./dist/` |
-| `npm run build:fast` | Build the site against a tiny fixture registry, without compiling full data |
-| `npm run preview` | Preview the build locally |
-| `npm run compile:data` | Read hand-authored YAML under `data/works/` and `data/systems/`, expand the in-memory registry, and emit JSONL resources plus `datapackage.json` under `dist/dump/` |
-| `npm run validate:data` | Validate every compiled record against the canonical Zod schemas |
-| `npm run build:data` | `compile:data` then `validate:data` — the contributor data pipeline |
-| `npm run verify:fast` | Fast local check using fixture registry data |
-| `npm run verify` | Prettier + `astro check` + tests + production build — the CI gate |
-| `npm run changelog` | Regenerate `CHANGELOG.md` from git history (git-cliff) |
+| Command | Action |
+| :---------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `npm install` | Install dependencies; wires git hooks (husky + lint-staged) |
+| `npm run dev` | Start local dev server at `localhost:4321` |
+| `npm run build` | Build the production site to `./dist/` |
+| `npm run build:fast` | Build the site against a tiny fixture registry, without compiling full data |
+| `npm run preview` | Preview the build locally |
+| `npm run compile:data` | Read hand-authored YAML under `data/works/` and `data/systems/`, expand the in-memory registry, and emit JSONL resources, the alias table, and `datapackage.json` under `dist/dump/` |
+| `npm run validate:data` | Validate every compiled record against the canonical Zod schemas |
+| `npm run build:data` | `compile:data` then `validate:data` — the contributor data pipeline |
+| `npm run verify:fast` | Fast local check using fixture registry data |
+| `npm run verify` | Prettier + `astro check` + tests + production build — the CI gate |
+| `npm run changelog` | Regenerate `CHANGELOG.md` from git history (git-cliff) |
Contributors edit the YAML under [`data/works/`](https://github.com/textrefs/registry/tree/main/works) and [`data/systems/`](https://github.com/textrefs/registry/tree/main/systems); the directory is a git submodule pointing at [`textrefs/registry`](https://github.com/textrefs/registry). Run `git submodule update --init --recursive` after cloning. The compiler expands the pinned submodule into the flat registry dump (works, systems, refs, mappings) under `dist/dump/`. See [`docs/get-started/authoring`](https://textrefs.org/get-started/authoring/) for the format. For documentation, styling, and route work, use `npm run verify:fast` locally; run the full `npm run verify` before PRs that touch registry data, release output, production build behaviour, or CI behaviour.
diff --git a/api/openapi.yaml b/api/openapi.yaml
index bea5b19..a1d71ab 100644
--- a/api/openapi.yaml
+++ b/api/openapi.yaml
@@ -23,16 +23,20 @@ info:
yet live.
A client that does not yet know a key starts at `/reg/works.json` or
- `/reg/systems.json`. `/dump/` carries the whole registry as
- newline-delimited JSON for bulk consumers.
+ `/reg/systems.json`. A client that knows a work key, a citation system
+ key, and a locator reads the reference UUID from
+ `/reg/work/{key}/aliases.json`. It needs no UUIDv5 implementation of its
+ own. `/dump/` carries the whole registry as newline-delimited JSON, plus
+ the whole alias table as one JSON object, for bulk consumers.
The site is static, and the host derives every `Content-Type` from the
file extension. This contract therefore declares the media type that
the host actually sends, not the media type that best describes the
- payload. A `.json` body is JSON-LD by content and arrives as
- `application/json`. A `.jsonl` body is newline-delimited JSON by
- content and arrives as `application/octet-stream`. Parse by
- documented shape, not by response header.
+ payload. A record or collection `.json` body is JSON-LD by content and
+ arrives as `application/json`. The two alias artifacts are plain JSON
+ and arrive the same way. A `.jsonl` body is newline-delimited JSON by
+ content and arrives as `application/octet-stream`. Parse by documented
+ shape, not by response header.
license:
name: AGPL-3.0-or-later
url: https://www.gnu.org/licenses/agpl-3.0.html
@@ -47,13 +51,15 @@ tags:
description: Canonical record URLs under `/id/`.
- name: Collections
description:
- Registry-wide JSON-LD collections under `/reg/`. Entry points for a
- client that does not yet know a key.
+ Registry-wide JSON-LD collections and per-work locator indexes under
+ `/reg/`. Entry points for a client that knows no key, or that knows a
+ locator but no reference UUID.
- name: Bulk
description:
Whole-registry artifacts under `/dump/`, rebuilt on every deploy. The
- host serves them as `application/octet-stream`; the bodies are
- newline-delimited JSON.
+ `.jsonl` bodies are newline-delimited JSON, and the host serves them
+ as `application/octet-stream`. The `.json` bodies are single JSON
+ objects, and the host serves them as `application/json`.
paths:
/id/work/{key}/:
get:
@@ -200,6 +206,28 @@ paths:
content:
application/json:
schema: { $ref: '#/components/schemas/SystemCollection' }
+ /reg/work/{key}/aliases.json:
+ get:
+ tags: [Collections]
+ summary: Work locator index — JSON
+ operationId: getWorkAliasIndex
+ description: Maps every locator of one work to the UUID of its
+ CanonicalReference, grouped by citation system key. A client that
+ knows a passage as work, citation system, and locator reads the UUID
+ here, then fetches `/id/ref/{uuid}.json`. It needs no UUIDv5
+ implementation of its own. The body is plain JSON, not JSON-LD, and
+ carries no `@context`.
+ parameters:
+ - $ref: '#/components/parameters/FlatKey'
+ responses:
+ '200':
+ description:
+ Every reference of this work, in every status, keyed by citation
+ system key and then by locator.
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/WorkAliasIndex' }
+ '404': { $ref: '#/components/responses/NotFound' }
/dump/works.jsonl:
get:
tags: [Bulk]
@@ -252,6 +280,23 @@ paths:
content:
application/octet-stream:
schema: { type: string }
+ /dump/aliases.json:
+ get:
+ tags: [Bulk]
+ summary: Alias table — JSON
+ operationId: getAliasesDump
+ description:
+ The complete alias table as one JSON object, with sorted keys and no
+ indentation. For a single lookup, use
+ `/reg/work/{key}/aliases.json` instead. This file serves a consumer
+ that mirrors the whole table.
+ responses:
+ '200':
+ description: One JSON object. Each key is an alias. Each value is the
+ canonical IRI that the alias resolves to.
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/AliasTable' }
/dump/datapackage.json:
get:
tags: [Bulk]
@@ -260,9 +305,10 @@ paths:
responses:
'200':
description:
- The Frictionless data-package descriptor that lists the four
- JSONL resources with, for each, its byte count and its
- `sha256:` hash, plus the registry version.
+ The Frictionless data-package descriptor that lists the five
+ resources — the four JSONL files and the JSON alias table —
+ with, for each, its byte count and its `sha256:` hash, plus the
+ registry version.
content:
application/json:
schema: { type: object }
@@ -481,6 +527,46 @@ components:
'@graph':
type: array
items: { $ref: '#/components/schemas/CitationSystem' }
+ WorkAliasIndex:
+ description:
+ The `/reg/work/{key}/aliases.json` locator index of one work. Plain
+ JSON, not JSON-LD; it carries no `@context`. The index states
+ identity alone. Read the CanonicalReference for the status and the
+ resolver targets.
+ type: object
+ required: [work_key, preferred_citation_system_key, refs]
+ properties:
+ work_key: { $ref: '#/components/schemas/FlatKey' }
+ preferred_citation_system_key:
+ allOf:
+ - $ref: '#/components/schemas/FlatKey'
+ description:
+ The system that the bare `/cite/{work_key}/{locator}` alias uses
+ (ADR-0005). A default for presentation, never an identity claim.
+ The other keys of `refs` have equal standing.
+ refs:
+ type: object
+ description: One entry per citation system key that this work carries
+ references under. An empty object means the work has no
+ references yet. The same locator under two systems denotes a
+ different passage, and a different UUID, each time.
+ additionalProperties:
+ type: object
+ description: Locator to CanonicalReference UUID.
+ additionalProperties:
+ type: string
+ pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
+ AliasTable:
+ description:
+ The `/dump/aliases.json` table, with one entry per alias. A key is
+ either a `/cite/` alias path — `{work_key}/{locator}` or
+ `{work_key}/{citation_system_key}/{locator}` — or an external
+ identifier IRI carried by a MappingAssertion. A value is the
+ canonical IRI that the key resolves to — an `/id/ref/` IRI for a
+ `/cite/` alias, an `/id/work/` IRI for an external identifier. A
+ `://` in the key marks the second kind.
+ type: object
+ additionalProperties: { $ref: '#/components/schemas/Iri' }
RegistryObject:
oneOf:
- { $ref: '#/components/schemas/Work' }
diff --git a/scripts/compile.test.ts b/scripts/compile.test.ts
index bfd2ded..63029fa 100644
--- a/scripts/compile.test.ts
+++ b/scripts/compile.test.ts
@@ -10,7 +10,13 @@ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { v5 as uuidv5 } from 'uuid';
-import { compileRegistry, type CompiledRegistry } from './compile.js';
+import { createHash } from 'node:crypto';
+import {
+ compileRegistry,
+ dumpResources,
+ describeResource,
+ type CompiledRegistry,
+} from './compile.js';
import { LanguageTag } from '../standard/schema/common.js';
type RegistryFiles = {
@@ -671,3 +677,100 @@ test('regular grandfathered tags parse and irregular ones do not', () => {
[],
);
});
+
+// --- Dump resources (#84) -------------------------------------------------
+
+// `workWithMappings` gives an alias table with both kinds of entry: `/cite/`
+// aliases targeting reference IRIs, and external identifiers targeting the work
+// IRI.
+const dumped = () => dumpResources(compileFixture(workWithMappings()));
+
+const aliasSpec = (specs: ReturnType) => {
+ const spec = specs.find((s) => s.name === 'aliases');
+ assert.ok(spec, 'the dump carries an alias resource');
+ return spec;
+};
+
+test('the dump carries the alias table beside the four JSONL resources', () => {
+ const specs = dumped();
+ assert.deepEqual(
+ specs.map((s) => s.name),
+ ['works', 'citation-systems', 'references', 'mappings', 'aliases'],
+ );
+ const spec = aliasSpec(specs);
+ assert.equal(spec.filename, 'aliases.json');
+ assert.equal(spec.format, 'json');
+ assert.equal(spec.mediatype, 'application/json');
+});
+
+test('the alias resource body is the whole alias table', () => {
+ const reg = compileFixture(workWithMappings());
+ const spec = aliasSpec(dumpResources(reg));
+ const table = JSON.parse(spec.body) as Record;
+ assert.deepEqual(table, reg.aliases);
+ // A `/cite/` alias resolves to a reference; an external identifier resolves
+ // to the work. A `://` in the key marks the second kind.
+ assert.match(
+ table['test.work/primary-section/5'],
+ /^https:\/\/textrefs\.org\/id\/ref\//,
+ );
+ assert.equal(
+ table['https://www.wikidata.org/entity/Q1'],
+ 'https://textrefs.org/id/work/test.work',
+ );
+});
+
+test('alias keys are sorted, so the body does not depend on visit order', () => {
+ const keys = Object.keys(JSON.parse(aliasSpec(dumped()).body));
+ assert.ok(keys.length > 1);
+ assert.deepEqual(keys, [...keys].sort());
+});
+
+test('the alias body is minified and newline-terminated', () => {
+ const body = aliasSpec(dumped()).body;
+ assert.equal(body.split('\n').length, 2);
+ assert.ok(body.startsWith('{"'));
+ assert.ok(body.endsWith('}\n'));
+});
+
+test('every descriptor states the bytes and sha256 of its own body', () => {
+ for (const spec of dumped()) {
+ const d = describeResource(spec);
+ // Recomputed the way a downstream consumer verifies a download.
+ assert.equal(d.bytes, Buffer.byteLength(spec.body, 'utf8'));
+ assert.equal(
+ d.hash,
+ `sha256:${createHash('sha256').update(spec.body).digest('hex')}`,
+ );
+ }
+});
+
+test('every descriptor carries the Frictionless fields', () => {
+ for (const spec of dumped()) {
+ const d = describeResource(spec);
+ assert.equal(d.profile, 'data-resource');
+ assert.equal(d.encoding, 'utf-8');
+ assert.equal(d.path, spec.filename);
+ assert.equal(d.name, spec.name);
+ }
+});
+
+test('the JSONL bodies are unchanged by the alias-resource refactor', () => {
+ const reg = compileFixture(workWithMappings());
+ const specs = dumpResources(reg);
+ const bodyOf = (name: string) => specs.find((s) => s.name === name)?.body;
+ assert.equal(
+ bodyOf('works'),
+ reg.works.map((r) => JSON.stringify(r)).join('\n') + '\n',
+ );
+ assert.equal(
+ bodyOf('references'),
+ reg.references.map((r) => JSON.stringify(r)).join('\n') + '\n',
+ );
+ // An empty record array still yields an empty body, not a bare newline.
+ assert.equal(
+ dumpResources({ ...reg, mappings: [] }).find((s) => s.name === 'mappings')
+ ?.body,
+ '',
+ );
+});
diff --git a/scripts/compile.ts b/scripts/compile.ts
index d0562ac..3e2563a 100644
--- a/scripts/compile.ts
+++ b/scripts/compile.ts
@@ -757,53 +757,109 @@ function readPackageVersion(): string {
return pkg.version;
}
-function writeDump(registry: CompiledRegistry, version: string): void {
- const dumpDir = join(distRoot, 'dump');
- mkdirSync(dumpDir, { recursive: true });
+type ResourceSpec = {
+ name: string;
+ filename: string;
+ format: string;
+ mediatype: string;
+ /**
+ * The pre-serialised body. The dump is no longer JSONL-only, so each
+ * resource carries its own bytes rather than a record array.
+ */
+ body: string;
+};
- type ResourceSpec = {
- name: string;
- filename: string;
- records: ReadonlyArray;
- };
+interface ResourceDescriptor {
+ name: string;
+ path: string;
+ profile: 'data-resource';
+ format: string;
+ mediatype: string;
+ encoding: 'utf-8';
+ bytes: number;
+ hash: string;
+}
- const specs: ResourceSpec[] = [
- { name: 'works', filename: 'works.jsonl', records: registry.works },
- {
- name: 'citation-systems',
- filename: 'citation-systems.jsonl',
- records: registry.systems,
- },
- {
- name: 'references',
- filename: 'references.jsonl',
- records: registry.references,
- },
+function jsonlBody(records: ReadonlyArray): string {
+ return records.length === 0
+ ? ''
+ : records.map((r) => JSON.stringify(r)).join('\n') + '\n';
+}
+
+/**
+ * Every `/dump/` resource body, in descriptor order. Pure — `writeDump` does
+ * the I/O — so the resource set, its media types, and its bodies are testable
+ * without a filesystem.
+ */
+export function dumpResources(registry: CompiledRegistry): ResourceSpec[] {
+ const jsonl = (
+ name: string,
+ filename: string,
+ records: ReadonlyArray,
+ ): ResourceSpec => ({
+ name,
+ filename,
+ format: 'jsonl',
+ mediatype: 'application/x-ndjson',
+ body: jsonlBody(records),
+ });
+
+ // The complete alias table (#84). Two kinds of entry share it: a `/cite/`
+ // alias targeting a reference IRI, and an external mapping identifier
+ // targeting a work IRI. Values stay full IRIs so a consumer can tell the two
+ // apart; a `://` in the key marks the second kind.
+ //
+ // Keys are sorted by code unit, so the body — and therefore its sha256 —
+ // depends on the registry content alone, never on the order the compiler
+ // happened to visit the work files in. No indentation: the body is ~13 MB.
+ const aliasBody =
+ JSON.stringify(
+ Object.fromEntries(
+ Object.entries(registry.aliases).sort(([a], [b]) =>
+ a < b ? -1 : a > b ? 1 : 0,
+ ),
+ ),
+ ) + '\n';
+
+ return [
+ jsonl('works', 'works.jsonl', registry.works),
+ jsonl('citation-systems', 'citation-systems.jsonl', registry.systems),
+ jsonl('references', 'references.jsonl', registry.references),
+ jsonl('mappings', 'mappings.jsonl', registry.mappings),
{
- name: 'mappings',
- filename: 'mappings.jsonl',
- records: registry.mappings,
+ name: 'aliases',
+ filename: 'aliases.json',
+ format: 'json',
+ mediatype: 'application/json',
+ body: aliasBody,
},
];
+}
- const resources = specs.map((spec) => {
- const body =
- spec.records.length === 0
- ? ''
- : spec.records.map((r) => JSON.stringify(r)).join('\n') + '\n';
- const bytes = Buffer.byteLength(body, 'utf8');
- const hash = createHash('sha256').update(body).digest('hex');
- writeFileSync(join(dumpDir, spec.filename), body);
- return {
- name: spec.name,
- path: spec.filename,
- profile: 'data-resource',
- format: 'jsonl',
- mediatype: 'application/x-ndjson',
- encoding: 'utf-8',
- bytes,
- hash: `sha256:${hash}`,
- };
+/**
+ * The Frictionless descriptor of one resource body: the byte count and the
+ * `sha256:` hash that a consumer recomputes to verify a download.
+ */
+export function describeResource(spec: ResourceSpec): ResourceDescriptor {
+ return {
+ name: spec.name,
+ path: spec.filename,
+ profile: 'data-resource',
+ format: spec.format,
+ mediatype: spec.mediatype,
+ encoding: 'utf-8',
+ bytes: Buffer.byteLength(spec.body, 'utf8'),
+ hash: `sha256:${createHash('sha256').update(spec.body).digest('hex')}`,
+ };
+}
+
+function writeDump(registry: CompiledRegistry, version: string): void {
+ const dumpDir = join(distRoot, 'dump');
+ mkdirSync(dumpDir, { recursive: true });
+
+ const resources = dumpResources(registry).map((spec) => {
+ writeFileSync(join(dumpDir, spec.filename), spec.body);
+ return describeResource(spec);
});
const datapackage = {
diff --git a/src/content/docs/get-started/authoring.md b/src/content/docs/get-started/authoring.md
index d41061d..0d6c216 100644
--- a/src/content/docs/get-started/authoring.md
+++ b/src/content/docs/get-started/authoring.md
@@ -7,7 +7,7 @@ sidebar:
Hand-authored YAML lives in the separate [`textrefs/registry`](https://github.com/textrefs/registry) repository. This repo mounts it as a git submodule at `data/`. After cloning, run `git submodule update --init --recursive`. See [`CONTRIBUTING.md`](https://github.com/textrefs/textrefs.org/blob/main/CONTRIBUTING.md) for the full workflow. Contributors edit `data/works/{work_key}.yaml` and `data/systems/{system_key}.yaml` there.
-`npm run build:data` produces the compiled registry from the pinned submodule pointer. The compiled registry holds Works, CitationSystems, CanonicalReferences, and MappingAssertions. The command writes them as JSONL resources plus `datapackage.json` under `dist/dump/`. Published dumps are attached to TextRefs Standard releases and site releases. They are also archived long-term in the [TextRefs Zenodo community](https://zenodo.org/communities/textrefs/) with citable DOIs.
+`npm run build:data` produces the compiled registry from the pinned submodule pointer. The compiled registry holds Works, CitationSystems, CanonicalReferences, and MappingAssertions. The command writes them as JSONL resources under `dist/dump/`, together with the alias table `aliases.json` and the descriptor `datapackage.json`. Published dumps are attached to TextRefs Standard releases and site releases. They are also archived long-term in the [TextRefs Zenodo community](https://zenodo.org/communities/textrefs/) with citable DOIs.
This page documents the YAML format.
@@ -426,7 +426,7 @@ Name your capture groups deliberately. Every URL template in every work that use
## Building, validating, and previewing
```sh
-npm run compile:data # expand YAML → JSONL resources plus datapackage.json under dist/dump/
+npm run compile:data # expand YAML → JSONL resources, aliases.json, and datapackage.json under dist/dump/
npm run validate:data # check every record against the canonical Zod schemas
npm run build:data # both, in order
npm run dev # browse at http://localhost:4321/reg/ ; records live under /id/
@@ -444,4 +444,4 @@ The compiler is deterministic. Re-running `compile:data` against unchanged sourc
- `/cite/{work_key}/{citation_system_key}/{locator}/`: qualified short alias, minted for every reference.
- `/cite/{work_key}/{locator}/`: a bare short alias, minted only for a work's preferred citation system. It MAY be retargeted if that preference changes.
-A reader who types `https://textrefs.org/cite/plato.republic/stephanus/514a` (qualified) or `https://textrefs.org/cite/plato.republic/514a` (bare) lands on the same canonical reference page. The compiler generates the alias index alongside the records. See [URL layout](/get-started/url-layout/) for the full four-prefix model and alias-permanence rules.
+A reader who types `https://textrefs.org/cite/plato.republic/stephanus/514a` (qualified) or `https://textrefs.org/cite/plato.republic/514a` (bare) lands on the same canonical reference page. The compiler generates the alias index alongside the records, and publishes it at `/reg/work/{work_key}/aliases.json` and `/dump/aliases.json`. See [URL layout](/get-started/url-layout/) for the full four-prefix model and alias-permanence rules.
diff --git a/src/content/docs/get-started/url-layout.md b/src/content/docs/get-started/url-layout.md
index 844948a..340e7bc 100644
--- a/src/content/docs/get-started/url-layout.md
+++ b/src/content/docs/get-started/url-layout.md
@@ -7,12 +7,12 @@ sidebar:
TextRefs uses four URL prefixes, each with one job. Together they make every registry record citeable, browsable, machine-readable, and short-linkable.
-| Prefix | Role | What lives there |
-| -------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `/id/` | **Identifier** | The canonical, persistent URL of every record. Each record is published twice: `/id/.../` (HTML) and a sibling `/id/....json` (JSON-LD). |
-| `/reg/` | **Browse** | The human registry browser: filter works and citation systems, then browse paginated reference lists from work pages. Links into `/id/`. Also serves two JSON-LD collections, so a client can discover records without a key. |
-| `/cite/` | **Cite** | Short, memorable URLs (`/cite/{work}/{system}/{locator}` always, and bare `/cite/{work}/{locator}` for the work's preferred system) that redirect to the canonical `/id/` URL. Convenience only. Bare aliases MAY be retargeted. `/id/` is permanent. |
-| `/api/` | **API docs** | The OpenAPI document that describes the `/id/` URL contract, plus the JSON-LD `@context` at `/contexts/`. |
+| Prefix | Role | What lives there |
+| -------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `/id/` | **Identifier** | The canonical, persistent URL of every record. Each record is published twice: `/id/.../` (HTML) and a sibling `/id/....json` (JSON-LD). |
+| `/reg/` | **Browse** | The human registry browser: filter works and citation systems, then browse paginated reference lists from work pages. Links into `/id/`. Also serves two JSON-LD collections and one locator index for each work, so a client can find a record without a key or a UUID. |
+| `/cite/` | **Cite** | Short, memorable URLs (`/cite/{work}/{system}/{locator}` always, and bare `/cite/{work}/{locator}` for the work's preferred system) that redirect to the canonical `/id/` URL. Convenience only. Bare aliases MAY be retargeted. `/id/` is permanent. |
+| `/api/` | **API docs** | The OpenAPI document that describes the `/id/` URL contract, plus the JSON-LD `@context` at `/contexts/`. |
In one line:
@@ -76,20 +76,60 @@ Each collection wraps its records in one `@context` and one `@graph` array:
Each item carries no `@context` of its own. The collection lists records of every status — active, draft, and retired — sorted by `key`.
-For bulk use, five artifacts live under `/dump/`:
+For bulk use, six artifacts live under `/dump/`:
- `https://textrefs.org/dump/works.jsonl`
- `https://textrefs.org/dump/citation-systems.jsonl`
- `https://textrefs.org/dump/references.jsonl`
- `https://textrefs.org/dump/mappings.jsonl`
-- `https://textrefs.org/dump/datapackage.json` — a Frictionless data-package descriptor. It lists the four JSONL resources with a byte count and a `sha256:` hash for each.
+- `https://textrefs.org/dump/aliases.json` — the complete alias table. Each key is a `/cite/` alias or an external identifier. Each value is the canonical `/id/` URL that the key resolves to.
+- `https://textrefs.org/dump/datapackage.json` — a Frictionless data-package descriptor. It lists the five resources with a byte count and a `sha256:` hash for each.
-Each `.jsonl` file holds one record per line, with no `@context`. This is the bulk-archive form of the registry.
+Each `.jsonl` file holds one record per line, with no `@context`. The alias table is one JSON object with sorted keys. Together they are the bulk-archive form of the registry.
-The site is static, and the host sets each `Content-Type` from the file extension. A `.json` body is JSON-LD by content, but it arrives as `application/json`. A `.jsonl` body arrives as `application/octet-stream`. Parse each body by its documented shape. Do not parse it by the response header.
+The site is static, and the host sets each `Content-Type` from the file extension. A record or collection `.json` body is JSON-LD by content, but it arrives as `application/json`. The two alias artifacts are plain JSON, and they arrive the same way. A `.jsonl` body arrives as `application/octet-stream`. Parse each body by its documented shape. Do not parse it by the response header.
Use the small JSON-LD collection for a browser client. Use the `/dump/` files for a bulk consumer. See [`/api/`](/api/) for the full contract.
+## Find a reference by its locator
+
+The UUID of a reference is a UUIDv5 of three fields: the work key, the citation system key, and the locator. A client can compute that UUID, but only with a UUIDv5 implementation. The registry therefore publishes the result as a static file.
+
+Each work has a locator index at `/reg/work/{work_key}/aliases.json`. The index maps each locator to the UUID of its canonical reference, grouped by citation system key. The body is plain JSON. It is not JSON-LD, and it carries no `@context`.
+
+To resolve a locator, do these steps:
+
+1. Send a GET request to `https://textrefs.org/reg/work/plato.republic/aliases.json`.
+2. Read the value at `refs["stephanus"]["514a"]`. The value is `dc799d4b-9b17-5d76-85aa-dfd001c5321d`.
+3. Send a GET request to `https://textrefs.org/id/ref/dc799d4b-9b17-5d76-85aa-dfd001c5321d.json`.
+
+Two requests give you the record. No UUID computation is necessary.
+
+The index has this shape:
+
+```json
+{
+ "work_key": "plato.republic",
+ "preferred_citation_system_key": "stephanus",
+ "refs": {
+ "stephanus": {
+ "327a": "cecef712-e8cf-5878-9d48-419f7d185a56",
+ "514a": "dc799d4b-9b17-5d76-85aa-dfd001c5321d"
+ }
+ }
+}
+```
+
+Obey these rules when you read an index:
+
+- Build the canonical URL yourself. Add the UUID to `https://textrefs.org/id/ref/`. Add `.json` for the JSON-LD record.
+- Use the citation system that you cite. A work with more than one system carries one entry in `refs` for each system. The same locator under two systems denotes a different passage each time.
+- Read `preferred_citation_system_key` only for the bare `/cite/{work_key}/{locator}` alias. It is a default for presentation. It does not change identity.
+- Do not read a status from the index. The index lists references of every status. Read the canonical record for the status and for the resolver targets.
+- Expect an empty `refs` object for a work that has no references yet.
+
+The largest index is about 1.2 MB, and most are smaller than 70 KB. Use `/dump/aliases.json` if you must resolve locators for many works at one time.
+
## Why four prefixes, not one
Persistent-identifier systems separate concerns. DOI and ORCID each have a canonical resolver URL that _is_ the API. Their documentation lives at a stable but distinct path. W3ID and PURL add short-alias redirects on top. TextRefs follows the same pattern:
diff --git a/src/content/docs/standard/versioning.md b/src/content/docs/standard/versioning.md
index ab569d4..a17a26a 100644
--- a/src/content/docs/standard/versioning.md
+++ b/src/content/docs/standard/versioning.md
@@ -53,6 +53,7 @@ dist/dump/works.jsonl
dist/dump/citation-systems.jsonl
dist/dump/references.jsonl
dist/dump/mappings.jsonl
+dist/dump/aliases.json
```
Registry exports are organized by object type. This gives consumers stable file names, simple streaming imports, and one predictable place to find each record type. Resolver targets are embedded in reference records. Relationships are represented inside records through standard fields such as `key`, `work_key`, `citation_system_key`, `subject`, and `target`.
@@ -69,7 +70,7 @@ Each `datapackage.json` MUST include:
- `name`: `textrefs-registry`.
- `version`: SemVer package version.
- `licenses`: SPDX identifier `CC0-1.0` for registry data.
-- `resources`: one resource per JSONL file, each carrying `path`, `format`, `mediatype`, `encoding`, `bytes`, and a `sha256:` `hash` of the resource body.
+- `resources`: one resource per published file, that is, each JSONL record file and the JSON alias table. Each resource carries `path`, `format`, `mediatype`, `encoding`, `bytes`, and a `sha256:` `hash` of the resource body.
Resources SHOULD additionally carry a Frictionless `schema` with field descriptors. Exports through `v0.1.0` omit it: record shapes are normative in the [specification](/standard/specification/) and enforced by the compiler, and restating them as Frictionless descriptors is [tracked separately](https://github.com/textrefs/textrefs.org/issues/74). Validate against the published shapes, not against the data package alone.
@@ -118,7 +119,7 @@ Two further checks apply. A work MUST NOT declare the same citation system twice
### Aliases vs. tombstones
-The compiler maintains a presentational alias map: multiple lookup paths pointing at the same canonical record.
+The compiler maintains a presentational alias map: multiple lookup paths pointing at the same canonical record. It publishes that map at `/dump/aliases.json`, and one per-work slice of it at `/reg/work/{work_key}/aliases.json`. Publishing the map does not change the permanence rules below.
- External identifiers.
- `{work_key}/{citation_system_key}/{locator}` — a qualified alias, minted for every reference.
diff --git a/src/lib/alias-index.test.ts b/src/lib/alias-index.test.ts
new file mode 100644
index 0000000..9671795
--- /dev/null
+++ b/src/lib/alias-index.test.ts
@@ -0,0 +1,150 @@
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { buildWorkAliasIndexes } from './alias-index.js';
+import { uuidOf } from './registry.js';
+import { fixtureRegistry } from './registry.fixture.js';
+
+const { works, references } = fixtureRegistry;
+const indexes = () => buildWorkAliasIndexes(works, references);
+
+// A synthetic reference, for the cases the fixture cannot express.
+const ref = (citation_system_key: string, locator: string, uuid: string) => ({
+ id: `https://textrefs.org/id/ref/${uuid}`,
+ work_key: 'fixture.work',
+ citation_system_key,
+ locator,
+});
+
+test('every reference of a work is reachable by system and locator', () => {
+ const [index] = indexes();
+ assert.equal(index.work_key, 'fixture.work');
+ for (const r of references) {
+ assert.ok(
+ index.refs[r.citation_system_key]?.[r.locator],
+ `unreachable: ${r.citation_system_key}/${r.locator}`,
+ );
+ }
+});
+
+test('each mapped value is the reference UUID, not its IRI', () => {
+ const [index] = indexes();
+ for (const r of references) {
+ const mapped = index.refs[r.citation_system_key][r.locator];
+ assert.equal(mapped, uuidOf(r.id));
+ assert.ok(!mapped.includes('/'), 'a UUID carries no path separator');
+ }
+});
+
+test('the same locator under two systems keeps two identities', () => {
+ // ADR-0005: a work under two systems can repeat a locator string and mean a
+ // different passage each time.
+ const [index] = indexes();
+ const a = index.refs['fixture-section']['1'];
+ const b = index.refs['fixture-alternate']['1'];
+ assert.ok(a && b);
+ assert.notEqual(a, b);
+});
+
+test('the index covers every reference exactly once', () => {
+ const mapped = indexes().reduce(
+ (total, index) =>
+ total +
+ Object.values(index.refs).reduce(
+ (n, locators) => n + Object.keys(locators).length,
+ 0,
+ ),
+ 0,
+ );
+ assert.equal(mapped, references.length);
+});
+
+test('a work with no references gets an index with an empty refs object', () => {
+ const [index] = buildWorkAliasIndexes(works, []);
+ assert.equal(index.work_key, 'fixture.work');
+ assert.deepEqual(index.refs, {});
+});
+
+test('the index names the preferred citation system', () => {
+ const [index] = indexes();
+ assert.equal(index.preferred_citation_system_key, 'fixture-section');
+ // The preferred key is presentation, not identity: both systems appear in
+ // `refs` with equal standing.
+ assert.deepEqual(Object.keys(index.refs).sort(), [
+ 'fixture-alternate',
+ 'fixture-section',
+ ]);
+});
+
+test('references of another work do not leak in', () => {
+ const foreign = {
+ id: 'https://textrefs.org/id/ref/00000000-0000-5000-8000-0000000000ff',
+ work_key: 'other.work',
+ citation_system_key: 'fixture-section',
+ locator: '99',
+ };
+ const built = buildWorkAliasIndexes(works, [...references, foreign]);
+ assert.equal(built.length, works.length);
+ assert.equal(built[0].refs['fixture-section']['99'], undefined);
+});
+
+test('draft references are indexed — the index makes no status claim', () => {
+ const draft = references.find((r) => r.status === 'draft');
+ assert.ok(draft, 'the fixture carries a draft reference');
+ const [index] = indexes();
+ assert.equal(
+ index.refs[draft.citation_system_key][draft.locator],
+ uuidOf(draft.id),
+ );
+});
+
+test('system keys sort, and locators sort numerically', () => {
+ const [index] = buildWorkAliasIndexes(works, [
+ ref('fixture-section', '1.10', '00000000-0000-5000-8000-00000000000a'),
+ ref('fixture-section', '1.2', '00000000-0000-5000-8000-00000000000b'),
+ ref('fixture-section', '1.1', '00000000-0000-5000-8000-00000000000c'),
+ ref('fixture-alternate', '1', '00000000-0000-5000-8000-00000000000d'),
+ ]);
+ assert.deepEqual(Object.keys(index.refs), [
+ 'fixture-alternate',
+ 'fixture-section',
+ ]);
+ assert.deepEqual(Object.keys(index.refs['fixture-section']), [
+ '1.1',
+ '1.2',
+ '1.10',
+ ]);
+});
+
+test('the output does not depend on the input order', () => {
+ const forward = buildWorkAliasIndexes(works, references);
+ const backward = buildWorkAliasIndexes(works, [...references].reverse());
+ assert.deepEqual(backward, forward);
+});
+
+test('a locator spelled __proto__ becomes an own key', () => {
+ const [index] = buildWorkAliasIndexes(works, [
+ ref('fixture-section', '__proto__', '00000000-0000-5000-8000-00000000000e'),
+ ]);
+ const locators = index.refs['fixture-section'];
+ assert.ok(Object.hasOwn(locators, '__proto__'));
+ assert.equal(Object.getPrototypeOf(locators), Object.prototype);
+ assert.equal(
+ JSON.parse(JSON.stringify(locators))['__proto__'],
+ '00000000-0000-5000-8000-00000000000e',
+ );
+});
+
+test('the caller’s arrays are not mutated', () => {
+ // `loadReferences()` hands back the live cached array.
+ const order = references.map((r) => r.id);
+ const keys = works.map((w) => w.key);
+ buildWorkAliasIndexes(works, references);
+ assert.deepEqual(
+ references.map((r) => r.id),
+ order,
+ );
+ assert.deepEqual(
+ works.map((w) => w.key),
+ keys,
+ );
+});
diff --git a/src/lib/alias-index.ts b/src/lib/alias-index.ts
new file mode 100644
index 0000000..f2b2ffa
--- /dev/null
+++ b/src/lib/alias-index.ts
@@ -0,0 +1,96 @@
+// Per-work locator indexes served at `/reg/work/{key}/aliases.json`.
+//
+// The only public handle on a reference is its UUIDv5, seeded from the work
+// key, the citation system key, and the locator (ADR-0002). A client that knows
+// a passage but ships no UUIDv5 implementation could reach the record only
+// through the `/cite/` redirect, which is HTML. This module turns the registry
+// inside out into one JSON map per work, so the same lookup needs a JSON parse
+// alone (#84).
+//
+// The indexes come from the reference records, not from `loadAliases()`. The
+// flat alias table mixes two target kinds — a `/cite/` alias targets a
+// reference IRI, an external mapping identifier targets a work IRI — and the
+// records already carry the three fields the index groups by.
+//
+// The logic sits here rather than in the page because the repository has no
+// Astro component test harness: `src/lib/*.test.ts` is where registry-shaped
+// logic is pinned.
+
+import { uuidOf } from './registry.js';
+
+// Locators sort the way the reference browser lists them — `1.2` before `1.10`.
+// The locale is pinned: the bare `localeCompare(undefined, …)` the Astro pages
+// use resolves against the host's ICU build, and a published file must not
+// depend on which machine built it.
+const byLocator = new Intl.Collator('en', { numeric: true }).compare;
+
+// Flat keys are ASCII, so code-unit order needs no collator and cannot drift.
+const byCodeUnit = (a: string, b: string): number =>
+ a < b ? -1 : a > b ? 1 : 0;
+
+export interface WorkAliasIndex {
+ work_key: string;
+ preferred_citation_system_key: string;
+ /** Citation system key → locator → CanonicalReference UUID. */
+ refs: Record>;
+}
+
+type IndexedWork = { key: string; preferred_citation_system_key: string };
+type IndexedReference = {
+ id: string;
+ work_key: string;
+ citation_system_key: string;
+ locator: string;
+};
+
+/**
+ * One index per work, in the order the works arrive.
+ *
+ * The map loses nothing, because the compiler makes `(work, system, locator)`
+ * unique: a block deduplicates its own locators, and a work cannot declare the
+ * same citation system twice.
+ */
+export function buildWorkAliasIndexes(
+ works: readonly IndexedWork[],
+ references: readonly IndexedReference[],
+): WorkAliasIndex[] {
+ const byWork = new Map();
+ for (const ref of references) {
+ const group = byWork.get(ref.work_key);
+ if (group) group.push(ref);
+ else byWork.set(ref.work_key, [ref]);
+ }
+
+ return works.map((work) => {
+ const bySystem = new Map();
+ for (const ref of byWork.get(work.key) ?? []) {
+ const group = bySystem.get(ref.citation_system_key);
+ if (group) group.push(ref);
+ else bySystem.set(ref.citation_system_key, [ref]);
+ }
+
+ const refs: Record> = {};
+ for (const [systemKey, group] of [...bySystem].sort(([a], [b]) =>
+ byCodeUnit(a, b),
+ )) {
+ // Sorting `group` mutates an array this function built, never the
+ // caller's array.
+ group.sort((a, b) => byLocator(a.locator, b.locator));
+ // `Object.fromEntries`, not assignment: a locator is author-controlled,
+ // and one spelled `__proto__` would set a prototype instead of a key.
+ // `fromEntries` defines own properties, so it cannot.
+ refs[systemKey] = Object.fromEntries(
+ group.map((ref) => [ref.locator, uuidOf(ref.id)]),
+ );
+ }
+
+ // Every work gets an index, including a work with no references. A client
+ // that holds a work key must be able to tell "no references yet" from
+ // "wrong key", and only a 200 with an empty `refs` does that.
+ return {
+ work_key: work.key,
+ preferred_citation_system_key: work.preferred_citation_system_key,
+ refs,
+ };
+ });
+}
diff --git a/src/pages/id/work/[key]/index.astro b/src/pages/id/work/[key]/index.astro
index 29960f9..8e549f1 100644
--- a/src/pages/id/work/[key]/index.astro
+++ b/src/pages/id/work/[key]/index.astro
@@ -103,6 +103,18 @@ const jsonHref = `/id/work/${work.key}.json`;
rel: 'alternate',
type: 'application/ld+json',
href: jsonHref,
+ title: 'Work record',
+ },
+ },
+ // The locator index that resolves {system, locator} to a reference
+ // UUID without a UUIDv5 implementation (#84).
+ {
+ tag: 'link',
+ attrs: {
+ rel: 'alternate',
+ type: 'application/json',
+ href: `/reg/work/${work.key}/aliases.json`,
+ title: 'Locator index',
},
},
// Draft records are excluded from search indexing (ADR-0003).
@@ -147,6 +159,10 @@ const jsonHref = `/id/work/${work.key}.json`;
Aliases ({aliases.length})
+
+ Every locator of this work maps to its reference UUID in the{' '}
+ locator index.
+
Mappings ({mappings.length})
{
diff --git a/src/pages/reg/work/[key]/aliases.json.ts b/src/pages/reg/work/[key]/aliases.json.ts
new file mode 100644
index 0000000..be4dcb6
--- /dev/null
+++ b/src/pages/reg/work/[key]/aliases.json.ts
@@ -0,0 +1,25 @@
+import type { APIRoute, GetStaticPaths } from 'astro';
+import { loadWorks, loadReferences } from '../../../../lib/registry.ts';
+import {
+ buildWorkAliasIndexes,
+ type WorkAliasIndex,
+} from '../../../../lib/alias-index.ts';
+
+// Everything comes from the compiled registry in memory. `npm run build` runs
+// `astro build` before `tsx scripts/compile.ts`, so `/dump/aliases.json` does
+// not exist yet at this point and this route must never read it.
+export const getStaticPaths: GetStaticPaths = () =>
+ buildWorkAliasIndexes(loadWorks(), loadReferences()).map((index) => ({
+ params: { key: index.work_key },
+ props: { index },
+ }));
+
+export const GET: APIRoute = ({ props }) => {
+ const { index } = props as { index: WorkAliasIndex };
+ // Minified, unlike the `/reg/*.json` collections: the largest index holds
+ // 23k locators and no person reads this file. Plain JSON, not JSON-LD — it
+ // carries no `@context` and states identity alone.
+ return new Response(JSON.stringify(index), {
+ headers: { 'Content-Type': 'application/json; charset=utf-8' },
+ });
+};
From cd9aae3effe028beeed7bc01a1570484b33281e9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
Date: Tue, 25 Aug 2026 10:06:49 +0200
Subject: [PATCH 44/45] feat(standard): name works by abbreviation and
translated title (#85) (#89)
A Work carried exactly one name. The registry browser built its filter
key from `preferred_label` and `key` only, so a scholar who typed "NE",
"EN", or "Nikomachische Ethik" found nothing.
Add an optional `alternative_labels: string[]` to Work, published as
`skos:altLabel`. The `skos` prefix was already declared, so no new
namespace is needed.
The field is identity-neutral. No label is a UUID seed input (ADR-0002),
so a label change never moves an identifier. A test locks this.
Entries are unique within one work, never repeat the preferred label,
and an empty list is rejected. All three are enforced on the authored
YAML and again on the published record, because the canonical schema is
what the standard publishes.
Record the decision and its four open questions in ADR-0007.
Closes #85.
---
api/openapi.yaml | 11 ++
.../ADR-0007-alternative-labels-for-works.md | 101 +++++++++++
public/contexts/v1.jsonld | 4 +
scripts/compile.test.ts | 160 ++++++++++++++++++
scripts/compile.ts | 3 +
scripts/source-schema.ts | 25 +++
src/content/docs/get-started/authoring.md | 34 +++-
src/content/docs/standard/json-ld.md | 1 +
src/content/docs/standard/specification.md | 6 +-
src/lib/registry.fixture.ts | 4 +
src/pages/id/work/[key]/index.astro | 14 ++
src/pages/reg/index.astro | 10 +-
standard/schema/work.ts | 30 ++++
13 files changed, 400 insertions(+), 3 deletions(-)
create mode 100644 decisions/ADR-0007-alternative-labels-for-works.md
diff --git a/api/openapi.yaml b/api/openapi.yaml
index a1d71ab..ee93adb 100644
--- a/api/openapi.yaml
+++ b/api/openapi.yaml
@@ -393,6 +393,17 @@ components:
key: { $ref: '#/components/schemas/FlatKey' }
type: { type: string, enum: [Work] }
preferred_label: { type: string, minLength: 1 }
+ alternative_labels:
+ type: array
+ minItems: 1
+ uniqueItems: true
+ items: { type: string, minLength: 1 }
+ description:
+ Additional names for the work — abbreviations, translated
+ titles, and established short forms. Search and display only.
+ Identity-neutral, because no label is a UUID seed input, so a
+ label change never moves an identifier. Unique within one work;
+ two different works MAY share an alternative label.
preferred_citation_system_key:
allOf:
- $ref: '#/components/schemas/FlatKey'
diff --git a/decisions/ADR-0007-alternative-labels-for-works.md b/decisions/ADR-0007-alternative-labels-for-works.md
new file mode 100644
index 0000000..847cbeb
--- /dev/null
+++ b/decisions/ADR-0007-alternative-labels-for-works.md
@@ -0,0 +1,101 @@
+# ADR-0007: Alternative labels on `Work` — one flat list, published as `skos:altLabel`
+
+- **Status:** Accepted
+- **Date:** 2026-08-24
+- **Deciders:** @maehr
+- **Tags:** spec, data-model
+
+## Context and problem statement
+
+A `Work` carries exactly one name. `standard/schema/work.ts:22` declares `preferred_label` as a plain string, and `public/contexts/v1.jsonld:16` maps it to `skos:prefLabel`. Nothing else names a work. There is no `alt_label`, no `abbreviation`, no `alternateName`, and no language-tagged label anywhere in `standard/`, `scripts/`, `data/`, or the published context. `alternateOf` is not a label field: it holds IRIs of other entities, the compiler derives it from `MappingAssertion`s, and an author never writes it (ADR-0006).
+
+The consequence shows in the registry browser. Before this change, `src/pages/reg/index.astro:71` built its filter key from `preferred_label` and `key` only (anchor against `staging` at `8bcfdca`), and the client filter — `src/pages/reg/index.astro:115-118` — is a plain substring match on that attribute. A scholar who typed `NE`, `EN`, or `Nikomachische Ethik` found nothing. The registry holds Aristotle's _Nicomachean Ethics_, but only under that one English string. The same gap hit `PI` for the _Philosophical Investigations_, `LXX` for the Septuagint, and every German, French, or Latin title a scholar uses by habit.
+
+Two documents already promised the fix and named a field that did not exist: `src/content/docs/get-started/authoring.md:124` and `data/AGENTS.md:36` both said "alt-names belong in a future `alt_labels` field". The promise was two documents old and never had a schema behind it.
+
+The registry is small today, so the cost is small. The roadmap plans growth across theology, classics, philosophy, and law. Works in those fields carry many established short forms. Search by one canonical English label does not scale to that.
+
+This adds a field to a published record shape, so it needs an ADR rather than a `standard:` erratum. [#85](https://github.com/textrefs/textrefs.org/issues/85) laid out the four options below and recommended option 2.
+
+## Decision drivers
+
+- The registry browser must find a work by the name a scholar actually types, which in classics and theology is usually an abbreviation, not the English title.
+- Labels must never reach identity. ADR-0002 fixes the reference UUID seed at `work_key`, `citation_system_key`, `locator`; no label appears in it, and none may be added.
+- The change must be additive. Every existing record lacks the field, and the build must pass with no submodule data change.
+- No new namespace. `skos` is already declared at `public/contexts/v1.jsonld:4` and already carries `preferred_label`.
+- The language question applies to `preferred_label` too. Answering it for one field alone would leave the two inconsistent and make the eventual fix breaking for both.
+
+## Considered options
+
+1. **Do nothing.** Keep one label per work; tell clients to search the `preferred_label` string. Rejected: it leaves the registry unusable for a scholar who knows a work by its abbreviation, which is the common case in the fields the roadmap targets.
+2. **One flat list of plain strings**, published as `skos:altLabel`. ← chosen
+3. **Language-tagged label objects** — `{ value: string, language?: string }[]`. `LanguageTag` already exists at `standard/schema/common.ts:41-46` for `ResolverTargetEntry`, so the validator is available. Rejected for now, not on merit: it is the better end state, but it leaves `preferred_label` a bare string, and the inconsistency means the language question should be answered for both fields at once. See _Open questions_.
+4. **Separate `alternative_labels` and `abbreviations`.** More expressive, because a search UI can give an abbreviation an exact match and a full title a substring match. Rejected: it doubles the authoring burden and the boundary is not clear. `LXX` is an abbreviation. `Septuaginta` is an alternate title. `Sept.` is arguable, and an author should not have to decide.
+
+## Decision
+
+We choose **option 2**. `Work` gains an optional `alternative_labels: string[]`, published as `skos:altLabel` with `"@container": "@set"`.
+
+The field is named **`alternative_labels`**, not the `alt_labels` that #85 and the two stale documents used. The registry spells its terms out — `preferred_label`, `preferred_citation_system_key`, `citation_system_key` — and an abbreviated key inside a field whose whole purpose is abbreviations reads as an accident.
+
+**Identity.** An alternative label is never a UUID seed input (ADR-0002). Adding, editing, or removing one MUST NOT move an identifier. `scripts/compile.test.ts` locks this: compiling the same work under two different label lists yields identical reference IRIs. This rule is non-negotiable and is stated normatively in `specification.md` §6.
+
+**Homonymy.** Two different works MAY claim the same alternative label. `Ethics` fits Aristotle and Spinoza. The compiler allows this and prints nothing. Homonymy is a fact about titles, not an authoring error, and the correct answer for a search field is two hits, not a rejected build. Consumers MUST treat such a match as ambiguous.
+
+**Authoring.** Inside one work, entries MUST be unique and MUST NOT repeat the `preferred_label`. An empty list is rejected too: omit the key instead. This keeps the uniqueness rule strictly local, which is what makes the homonymy allowance above coherent.
+
+These rules are enforced **twice**, because the two schemas answer different questions. `WorkSource.superRefine` (`scripts/source-schema.ts:191-207`) rejects the authored YAML at parse time with a field path, beside the duplicate-citation-system check. `Work.superRefine` (`standard/schema/work.ts:59-75`) rejects the compiled record, and it is the canonical schema the standard publishes — `scripts/validate-data.ts:36` and `scripts/compile.ts:519` both run it. Enforcing only the authoring shape would let a hand-built or third-party record carry a duplicate while still validating against the published schema, which would make the normative sentence in `specification.md` §6 unenforceable. The two report the same two messages, so a violation reads the same whichever layer catches it.
+
+**Scope.** `CitationSystem` does not get the field in this ADR, even though it has the same problem — `Bekker` and `Stephanus` are the names scholars use, not the registry's `preferred_label` strings. One decision per ADR. See _Open questions_.
+
+**Versioning.** No spec version bump. The standard stays `v0.1.0-draft` (`ROADMAP.md:7`, `specification.md:9`) and carries `maturity: working-draft`, which `/standard/versioning/` defines as "Unstable. Data model and prose may change without notice and without a version bump while the core is settled." ADR-0004 and ADR-0006 both invoked this clause; this ADR follows that precedent. The **data package** is a separate artefact and takes a **minor** SemVer bump at the next release under `/standard/versioning/#semver-rules-for-data-packages` — "Compatible new fields require a minor version increment" — which is automatic, since the compiler reads the version from `package.json`.
+
+## Consequences
+
+### Positive
+
+- The registry browser finds a work by abbreviation and by translated title. This needed no client script change: the labels are folded into the existing `data-reg-item` haystack.
+- The field flows into `dist/dump/works.jsonl`, `/reg/works.json`, and `/id/work/{key}.json` with no further edit. `scripts/compile.ts:786-790`, `src/lib/collection.ts:11-15`, and `src/pages/id/work/[key].json.ts:15` all serialize the whole record rather than a field whitelist.
+- `skos:altLabel` is standard vocabulary paired with the `skos:prefLabel` already in the context, so consuming tools understand it without documentation and no new namespace is added.
+- The two stale `alt_labels` promises can finally be honoured.
+- Additive and optional, so no existing record changes and no identifier moves. The gate that would have caught an omission — `scripts/validate-data.ts:81-114`, which fails on any emitted key absent from the context — passes.
+
+### Negative / trade-offs
+
+- Every work record grows an optional field that reviewers must check for accuracy. An alternative label is a curatorial claim, like a mapping, but unlike a mapping it carries no `source` and no `status`. It is asserted by the record and reviewed only in the pull request.
+- Plain strings carry no language tag, so `Nikomachische Ethik` and `Ethica Nicomachea` sit in the same undifferentiated list. A client cannot filter labels by language or render the right one for its locale.
+- If a later ADR moves to language-tagged objects, the field shape breaks for existing consumers. Deferring the question is the cost paid for shipping search in one release.
+- Allowing cross-work duplicates means the browser can return two hits for one query with nothing to disambiguate them beyond the `key` shown in the row. That is correct, but it is a UI problem this ADR does not solve.
+- Nothing checks `public/contexts/v1.jsonld` against its hand-maintained copy at `src/content/docs/standard/json-ld.md:58`. This ADR adds one more term that can drift between them.
+
+### Follow-up actions
+
+- [x] Add `alternative_labels: z.array(z.string().min(1)).optional()` to `WorkBase` in `standard/schema/work.ts`, after `preferred_label`.
+- [x] Add `"alternative_labels": { "@id": "skos:altLabel", "@container": "@set" }` to `public/contexts/v1.jsonld`, and the same term to the copy in `src/content/docs/standard/json-ld.md`.
+- [x] Add the field to the strict `work` block of `WorkSource` in `scripts/source-schema.ts` with `.min(1)`, plus the uniqueness check in its `superRefine`. Mirror both in the canonical `Work` schema, so the published record shape enforces what the specification asserts.
+- [x] Project the field onto the compiled record in `scripts/compile.ts`, following the conditional-spread pattern the `creators` field uses. The record is built from an explicit field list, so an unlisted key is silently dropped.
+- [x] Mirror the field on the `Work` schema in `api/openapi.yaml`, as an optional array carrying `minItems: 1` and `uniqueItems: true`, so a downstream client validates against the same constraints.
+- [x] Fold the labels into the browser filter key in `src/pages/reg/index.astro`, and render them on `src/pages/id/work/[key]/index.astro`.
+- [x] Update `specification.md` — the mermaid `class Work` block, the `plato.republic` example, the `Optional:` list, and one normative paragraph in §6 stating the identity and homonymy rules.
+- [x] Replace the `alt_labels` promise in `src/content/docs/get-started/authoring.md` with a real `### alternative_labels` section.
+- [x] Add the field to the fixture work in `src/lib/registry.fixture.ts`, so `npm run build:fast` exercises both the record page and the filter without submodule data.
+- [x] Extend `scripts/compile.test.ts`: projection, omission, empty list, in-work duplicate, a label equal to `preferred_label`, a shared label across two works, and identifier stability under a label change — through the YAML path, and again directly against the canonical `Work` schema.
+- [ ] Fix the stale `alt_labels` promise at `data/AGENTS.md:36`. That file lives in `textrefs/registry`, so it needs a pull request to the submodule repository.
+- [ ] Seed alternative labels on `data/works/*.yaml` in a `textrefs/registry` pull request — `NE`/`EN` for the _Nicomachean Ethics_, `PI` for the _Philosophical Investigations_ — then bump the submodule pointer here. Follow the workflow in `CONTRIBUTING.md:135-140`.
+- [ ] **Separate ADR or issue:** disambiguate a duplicate hit in the registry browser. Two works sharing a label is legal by this decision, and the row shows only the `key`.
+- [ ] **Separate ADR:** language tagging for labels, answered for `preferred_label` and `alternative_labels` together. See _Open questions_ 1.
+
+## Open questions
+
+Recorded, not answered here.
+
+1. **Plain strings now, or language-tagged objects?** Deferring costs a breaking record-shape change later: consumers reading `alternative_labels` as `string[]` would have to move to `{ value, language }[]`. Answering it now would leave `preferred_label` a bare string and the two fields inconsistent, and fixing that inconsistency is itself breaking. The question belongs to both fields at once.
+2. **One flat list, or separate `abbreviations` and alternate titles?** A search UI treats them differently — an abbreviation deserves an exact match, a full title a substring match. Option 4 above rejected the split on authoring cost, not on merit. Revisit if the browser gains ranked search.
+3. **Does `CitationSystem` need the same field?** It has the same problem: `Bekker` and `Stephanus` are the names scholars use. Out of scope for v1. If the answer is yes, it is additive and non-breaking at that point.
+4. **Is there an upper bound on the number of labels per work?** None is enforced. A work with thirty labels would bloat the filter haystack and the dump without an obvious benefit. No evidence yet on where a sensible limit sits.
+
+## Links
+
+- Related ADRs: ADR-0002 (the UUID seed — the reason a label can never move an identifier), ADR-0005 (introduces the Zod authoring schema this field extends), ADR-0006 (`alternateOf` is a mapping projection, not a label field)
+- Related issues / PRs: textrefs/textrefs.org#85
+- External references: [SKOS `altLabel`](https://www.w3.org/TR/skos-reference/#labels), [JSON-LD `@container: @set`](https://www.w3.org/TR/json-ld11/#sets-and-lists)
diff --git a/public/contexts/v1.jsonld b/public/contexts/v1.jsonld
index 7884ea3..ce16c99 100644
--- a/public/contexts/v1.jsonld
+++ b/public/contexts/v1.jsonld
@@ -14,6 +14,10 @@
"MappingAssertion": "tr:MappingAssertion",
"key": "tr:key",
"preferred_label": "skos:prefLabel",
+ "alternative_labels": {
+ "@id": "skos:altLabel",
+ "@container": "@set"
+ },
"description": "dcterms:description",
"creators": "schema:creator",
"kind": "tr:creatorKind",
diff --git a/scripts/compile.test.ts b/scripts/compile.test.ts
index 63029fa..dffad98 100644
--- a/scripts/compile.test.ts
+++ b/scripts/compile.test.ts
@@ -18,6 +18,7 @@ import {
type CompiledRegistry,
} from './compile.js';
import { LanguageTag } from '../standard/schema/common.js';
+import { Work } from '../standard/schema/work.js';
type RegistryFiles = {
systems: Record;
@@ -267,6 +268,165 @@ references:
}
});
+// --- Alternative labels (ADR-0007) ----------------------------------------
+
+/** A work header whose `work:` block carries an `alternative_labels:` list. */
+const workWithLabels = (labels: string[]) => ({
+ systems: twoSystems,
+ works: {
+ 'test.work': `
+work:
+ key: test.work
+ preferred_label: Test Work
+ alternative_labels:
+${labels.map((l) => ` - '${l}'`).join('\n')}
+ status: active
+ created: 2026-01-01
+ modified: 2026-01-01
+
+citation_system: primary-section
+references:
+ - '5'
+`,
+ },
+});
+
+test('authored alternative labels reach the compiled work in order', () => {
+ const reg = compileFixture(workWithLabels(['TW', 'Testwerk']));
+ const work = reg.works.find((w) => w.key === 'test.work');
+ assert.ok(work);
+ assert.deepEqual(work.alternative_labels, ['TW', 'Testwerk']);
+});
+
+test('a work without alternative labels omits the key entirely', () => {
+ const reg = compileFixture({
+ systems: twoSystems,
+ works: {
+ 'test.work': `${workHeader()}
+citation_system: primary-section
+references:
+ - '5'
+`,
+ },
+ });
+ const work = reg.works.find((w) => w.key === 'test.work');
+ assert.ok(work);
+ assert.ok(!('alternative_labels' in work));
+});
+
+test('an empty alternative_labels list is an authoring error', (t) => {
+ const message = expectCompileError(t, {
+ systems: twoSystems,
+ works: {
+ 'test.work': `
+work:
+ key: test.work
+ preferred_label: Test Work
+ alternative_labels: []
+ status: active
+ created: 2026-01-01
+ modified: 2026-01-01
+
+citation_system: primary-section
+references:
+ - '5'
+`,
+ },
+ });
+ assert.match(message, /invalid source file/);
+});
+
+test('repeating one alternative label within a work is an authoring error', (t) => {
+ const message = expectCompileError(t, workWithLabels(['TW', 'TW']));
+ assert.match(message, /invalid source file/);
+});
+
+test('an alternative label equal to the preferred label is an authoring error', (t) => {
+ const message = expectCompileError(t, workWithLabels(['Test Work']));
+ assert.match(message, /invalid source file/);
+});
+
+test('two different works may share one alternative label', () => {
+ const shared = (key: string, label: string) => `
+work:
+ key: ${key}
+ preferred_label: ${label}
+ alternative_labels:
+ - 'Ethics'
+ status: active
+ created: 2026-01-01
+ modified: 2026-01-01
+
+citation_system: primary-section
+references:
+ - '5'
+`;
+ const reg = compileFixture({
+ systems: twoSystems,
+ works: {
+ 'aristotle.ne': shared('aristotle.ne', 'Nicomachean Ethics'),
+ 'spinoza.ethica': shared('spinoza.ethica', 'Ethica'),
+ },
+ });
+ const claimants = reg.works.filter((w) =>
+ (w.alternative_labels ?? []).includes('Ethics'),
+ );
+ assert.equal(claimants.length, 2);
+});
+
+// An alternative label is not a UUID seed input (ADR-0002), so editing one
+// must leave every minted identifier where it was.
+test('alternative labels never move a reference identifier', () => {
+ const withLabels = compileFixture(workWithLabels(['TW']));
+ const withOtherLabels = compileFixture(workWithLabels(['Testwerk', 'TW-2']));
+ assert.deepEqual(
+ withLabels.references.map((r) => r.id),
+ withOtherLabels.references.map((r) => r.id),
+ );
+});
+
+// The rules above are enforced on the published record too, not only on the
+// authored YAML — a hand-built record that reaches a consumer must not be able
+// to carry an empty list, a repeat, or the preferred label.
+const workRecord = (alternative_labels?: string[]) => ({
+ id: 'https://textrefs.org/id/work/test.work',
+ key: 'test.work',
+ type: 'Work' as const,
+ preferred_label: 'Test Work',
+ ...(alternative_labels ? { alternative_labels } : {}),
+ preferred_citation_system_key: 'primary-section',
+ status: 'active' as const,
+ created: '2026-01-01',
+ modified: '2026-01-01',
+});
+
+test('the canonical Work schema accepts a well-formed label list', () => {
+ assert.equal(Work.safeParse(workRecord(['TW', 'Testwerk'])).success, true);
+ assert.equal(Work.safeParse(workRecord()).success, true);
+});
+
+test('the canonical Work schema rejects an empty label list', () => {
+ assert.equal(Work.safeParse(workRecord([])).success, false);
+});
+
+test('the canonical Work schema rejects a repeated label', () => {
+ const result = Work.safeParse(workRecord(['TW', 'TW']));
+ assert.equal(result.success, false);
+ assert.match(
+ result.error?.issues[0]?.message ?? '',
+ /declared more than once/,
+ );
+});
+
+test('the canonical Work schema rejects a label equal to preferred_label', () => {
+ const result = Work.safeParse(workRecord(['Test Work']));
+ assert.equal(result.success, false);
+ assert.match(
+ result.error?.issues[0]?.message ?? '',
+ /repeats the preferred label/,
+ );
+});
+
// --- Mapping relation vocabulary (ADR-0006) -------------------------------
const mappings = `
diff --git a/scripts/compile.ts b/scripts/compile.ts
index 3e2563a..236b54d 100644
--- a/scripts/compile.ts
+++ b/scripts/compile.ts
@@ -499,6 +499,9 @@ export function compileRegistry(dataRootOverride?: string): CompiledRegistry {
key: workKey,
type: 'Work' as const,
preferred_label: src.work.preferred_label,
+ ...(src.work.alternative_labels
+ ? { alternative_labels: src.work.alternative_labels }
+ : {}),
// The top-level `citation_system:` block is the preferred one
// (ADR-0005); it is what mints the bare `/cite/{work}/{locator}` alias.
preferred_citation_system_key: systemKey,
diff --git a/scripts/source-schema.ts b/scripts/source-schema.ts
index 717abd3..d9cb93a 100644
--- a/scripts/source-schema.ts
+++ b/scripts/source-schema.ts
@@ -166,6 +166,9 @@ export const WorkSource = z
work: z.strictObject({
key: FlatKey,
preferred_label: z.string().min(1),
+ // Abbreviations, translated titles, and established short forms.
+ // Omit the key rather than authoring an empty list.
+ alternative_labels: z.array(z.string().min(1)).min(1).optional(),
status: Status,
created: IsoDate,
modified: IsoDate,
@@ -181,6 +184,28 @@ export const WorkSource = z
additional_systems: z.array(AdditionalSystemSource).optional(),
})
.superRefine((src, ctx) => {
+ // Alternative labels are unique inside one work, and none of them
+ // repeats the preferred label. Two different works MAY share a label —
+ // "Ethics" fits Aristotle and Spinoza — so this check stays local
+ // (ADR-0007).
+ const seenLabels = new Set();
+ (src.work.alternative_labels ?? []).forEach((label, i) => {
+ if (label === src.work.preferred_label) {
+ ctx.addIssue({
+ code: 'custom',
+ message: `alternative label "${label}" repeats the preferred label`,
+ path: ['work', 'alternative_labels', i],
+ });
+ } else if (seenLabels.has(label)) {
+ ctx.addIssue({
+ code: 'custom',
+ message: `alternative label "${label}" is declared more than once for this work`,
+ path: ['work', 'alternative_labels', i],
+ });
+ }
+ seenLabels.add(label);
+ });
+
const seen = new Set([src.citation_system]);
(src.additional_systems ?? []).forEach((block, i) => {
if (seen.has(block.citation_system)) {
diff --git a/src/content/docs/get-started/authoring.md b/src/content/docs/get-started/authoring.md
index 0d6c216..a176442 100644
--- a/src/content/docs/get-started/authoring.md
+++ b/src/content/docs/get-started/authoring.md
@@ -38,6 +38,9 @@ The Dhammapada has 423 verses across 26 chapters and is hosted on two different
work:
key: dhammapada
preferred_label: Dhammapada
+ alternative_labels:
+ - 'Dhp'
+ - 'Dhammapāda'
status: draft
created: 2026-05-31
modified: 2026-05-31
@@ -121,13 +124,42 @@ The display title. Do not add a parenthetical disambiguator:
- the author goes in `creators`;
- the edition, such as SBLGNT or OCT, goes on the resolver target;
-- alt-names belong in a future `alt_labels` field.
+- alt-names go in `alternative_labels`.
Use these forms:
- Attributed works: just the title, for example `Iliad`, `Republic`, `Tractatus Logico-Philosophicus`.
- Anonymous or collective works: the conventional English name, for example `Tanakh`, `Dhammapada`, `New Testament`.
+### `alternative_labels`
+
+The other names a scholar searches by. The registry browser matches this list, so a reader who types `NE` or `Nikomachische Ethik` still finds the work.
+
+```yaml
+work:
+ key: aristotle.nicomachean-ethics
+ preferred_label: Nicomachean Ethics
+ alternative_labels:
+ - 'NE'
+ - 'EN'
+ - 'Ethica Nicomachea'
+ - 'Nikomachische Ethik'
+```
+
+Rules:
+
+- Add established forms only: a recognised abbreviation, a Latin or Greek title, or a translated title in a language scholars cite in. Do not invent a short form.
+- Keep each entry unique inside the work. Do not repeat the `preferred_label`. The compiler rejects both.
+- Two different works may share an entry. `Ethics` fits Aristotle and Spinoza. This is allowed.
+- Omit the key when there is nothing to add. Do not author an empty list.
+- The field never changes an identifier. It is not part of any UUID seed, so you may add or correct a label at any time.
+
+Do not put these in the list:
+
+- the author, which goes in `creators`;
+- the edition, such as SBLGNT or OCT, which goes on the resolver target;
+- an external identifier, such as a Wikidata Q-ID, which goes in `mappings`.
+
### `creators`
Follow CSL-JSON conventions so citeproc-js and Zotero render correctly.
diff --git a/src/content/docs/standard/json-ld.md b/src/content/docs/standard/json-ld.md
index 31bb0c7..a400f10 100644
--- a/src/content/docs/standard/json-ld.md
+++ b/src/content/docs/standard/json-ld.md
@@ -55,6 +55,7 @@ Choose `alternateOf` when the target identifies the same work from a different p
"MappingAssertion": "tr:MappingAssertion",
"key": "tr:key",
"preferred_label": "skos:prefLabel",
+ "alternative_labels": { "@id": "skos:altLabel", "@container": "@set" },
"description": "dcterms:description",
"creators": "schema:creator",
"kind": "tr:creatorKind",
diff --git a/src/content/docs/standard/specification.md b/src/content/docs/standard/specification.md
index ac7cb8c..38440fa 100644
--- a/src/content/docs/standard/specification.md
+++ b/src/content/docs/standard/specification.md
@@ -68,6 +68,7 @@ classDiagram
+URI id
+string key
+string preferred_label
+ +string[] alternative_labels
+string preferred_citation_system_key
+Creator[] creators
}
@@ -121,6 +122,7 @@ A `Work.key` is a single flat registry key used to identify the abstract work in
"key": "plato.republic",
"type": "Work",
"preferred_label": "Republic",
+ "alternative_labels": ["Politeia", "Der Staat"],
"preferred_citation_system_key": "stephanus",
"creators": [{ "kind": "person", "family": "Plato" }],
"status": "active",
@@ -129,13 +131,15 @@ A `Work.key` is a single flat registry key used to identify the abstract work in
}
```
-Required: `id`, `key`, `type` (`Work`), `preferred_label`, `preferred_citation_system_key`, `status`, plus administrative metadata ([§12](#12-administrative-metadata)). Optional: `creators`, `alternateOf`, `isReferencedBy`. The `id` MUST be a persistent TextRefs HTTP URI of the form `https://textrefs.org/id/work/{key}`, where `{key}` is one flat key and occupies exactly one URI path segment. The `key` MUST be stable and suitable for deterministic identity generation.
+Required: `id`, `key`, `type` (`Work`), `preferred_label`, `preferred_citation_system_key`, `status`, plus administrative metadata ([§12](#12-administrative-metadata)). Optional: `alternative_labels`, `creators`, `alternateOf`, `isReferencedBy`. The `id` MUST be a persistent TextRefs HTTP URI of the form `https://textrefs.org/id/work/{key}`, where `{key}` is one flat key and occupies exactly one URI path segment. The `key` MUST be stable and suitable for deterministic identity generation.
- `preferred_citation_system_key` MUST reference a known `CitationSystem`. It governs the bare `/cite/{work_key}/{locator}/` alias and default presentation only; it is identity-neutral and MUST NOT affect the validation or resolution of a fully qualified reference ([§7](#7-citationsystem)).
- A `Work` MAY be referenced under more than one `CitationSystem`, of which exactly one is preferred.
`creators`, when present, is an array of entries discriminated by `kind`. A `person` entry has `family` (required) and `given` (optional); mononyms such as Plato or Homer use `family` alone, following CSL convention. A `literal` entry has `name` and is the escape hatch for pseudonymous, collective, or institutional authorship (e.g. `[Pseudo-]Aristotle`, an editorial committee). Anonymous works and canonical corpora such as the Bible simply omit `creators`. For attributed-but-disputed works, record the traditional attribution in `creators` and express uncertainty through mappings or editorial review notes, not in the name string. Implementations MUST treat the field as optional and MUST NOT infer authorship from `preferred_label` or `key`.
+`alternative_labels`, when present, is an array of additional names for the work: abbreviations such as `NE`, translated titles such as `Nikomachische Ethik`, and other established short forms. It is published as `skos:altLabel`. The field serves search and display only. It is identity-neutral: no label is an input to any deterministic UUID seed ([§11](#11-identifier-policy)), so adding, editing, or removing an alternative label MUST NOT move an identifier. Each entry MUST be unique within the work and MUST NOT repeat the `preferred_label`. A work with no additional names MUST omit the field rather than publish an empty array. Two different works MAY claim the same alternative label — `Ethics` fits both Aristotle and Spinoza — and implementations MUST treat such a match as ambiguous rather than as an error. Implementations MUST NOT derive identity, authorship, or a citation system from an alternative label.
+
External identifiers for a `Work` (e.g. Wikidata Q-ID, DOI, VIAF) are asserted as `MappingAssertion`s whose `subject` is the `Work` ([§10](#10-mappingassertion)). They MUST NOT be authored directly on the `Work`.
`alternateOf` and `isReferencedBy` are the compiler's read-only projection of those assertions — every one whose `status` is not `deprecated`, `withdrawn` or `blocked`, grouped by `relation` — published straight from the work IRI as `prov:alternateOf` / `dcterms:isReferencedBy` edges ([JSON-LD](/standard/json-ld/#mapping-relations)). They carry no status or provenance; the `MappingAssertion` stays authoritative. The arrays enrich the work. They make no claim about review: a consumer that needs the status of a mapping MUST read the `MappingAssertion`.
diff --git a/src/lib/registry.fixture.ts b/src/lib/registry.fixture.ts
index 29cd87a..e6bdf0a 100644
--- a/src/lib/registry.fixture.ts
+++ b/src/lib/registry.fixture.ts
@@ -15,6 +15,10 @@ export const fixtureRegistry: CompiledRegistry = {
key: 'fixture.work',
type: 'Work',
preferred_label: 'Fixture Work',
+ // Exercises the ADR-0007 field under `build:fast`: the record page
+ // renders the list, and the registry browser finds the work by
+ // "fw" and by "Musterwerk".
+ alternative_labels: ['FW', 'Musterwerk'],
preferred_citation_system_key: 'fixture-section',
status: 'active',
created: '2026-01-01',
diff --git a/src/pages/id/work/[key]/index.astro b/src/pages/id/work/[key]/index.astro
index 8e549f1..49f8adb 100644
--- a/src/pages/id/work/[key]/index.astro
+++ b/src/pages/id/work/[key]/index.astro
@@ -87,6 +87,7 @@ const {
aliases,
} = Astro.props;
const jsonHref = `/id/work/${work.key}.json`;
+const alternativeLabels = work.alternative_labels ?? [];
---
+ {
+ alternativeLabels.length > 0 && (
+
+
Alternative labels ({alternativeLabels.length})
+
+ {alternativeLabels.map((label) => (
+
{label}
+ ))}
+
+
+ )
+ }
+
Citation systems
{
systems.length === 0 ? (
diff --git a/src/pages/reg/index.astro b/src/pages/reg/index.astro
index 936811b..9f35d53 100644
--- a/src/pages/reg/index.astro
+++ b/src/pages/reg/index.astro
@@ -6,6 +6,14 @@ import { byKey } from '../../lib/collection.ts';
const works = [...loadWorks()].sort(byKey);
const systems = [...loadSystems()].sort(byKey);
+
+// The client filter below is a plain substring match on this attribute, so a
+// work is found by its abbreviation and by a translated title as soon as those
+// sit in the haystack (ADR-0007).
+const workFilterKey = (w: (typeof works)[number]) =>
+ [w.preferred_label, ...(w.alternative_labels ?? []), w.key]
+ .join(' ')
+ .toLowerCase();
---
{works.map((w) => (
-
+
{w.preferred_label}{' '}
{w.status}{' '}
{w.key}
diff --git a/standard/schema/work.ts b/standard/schema/work.ts
index 0d64af1..1a1c063 100644
--- a/standard/schema/work.ts
+++ b/standard/schema/work.ts
@@ -20,6 +20,14 @@ export const WorkBase = AdminMetadata.extend({
key: FlatKey,
type: z.literal('Work'),
preferred_label: z.string().min(1),
+ // Additional names for the work: abbreviations ("NE"), translated titles
+ // ("Nikomachische Ethik"), and established short forms. Search and display
+ // only. Identity-neutral: no label is a UUID seed input (ADR-0002), so a
+ // label change never moves an identifier (ADR-0007). Two works MAY share an
+ // alternative label; the compiler does not treat that as an error. Omit the
+ // field rather than publishing an empty array; the entries are checked for
+ // uniqueness below.
+ alternative_labels: z.array(z.string().min(1)).min(1).optional(),
// The citation system this work is cited under by default (ADR-0005). It
// governs the bare `/cite/{work}/{locator}` alias and default presentation
// only — it is identity-neutral and never affects how a fully qualified
@@ -43,6 +51,28 @@ export const Work = WorkBase.superRefine((w, ctx) => {
path: ['id'],
});
}
+ // The rules the specification states for alternative_labels (§6). They are
+ // enforced here, on the published record, and not only on the authored
+ // YAML, so a record that reaches a consumer cannot carry a label twice.
+ // Uniqueness is deliberately local to one work: two different works MAY
+ // claim the same label (ADR-0007).
+ const seenLabels = new Set();
+ (w.alternative_labels ?? []).forEach((label, i) => {
+ if (label === w.preferred_label) {
+ ctx.addIssue({
+ code: 'custom',
+ message: `alternative label "${label}" repeats the preferred label`,
+ path: ['alternative_labels', i],
+ });
+ } else if (seenLabels.has(label)) {
+ ctx.addIssue({
+ code: 'custom',
+ message: `alternative label "${label}" is declared more than once for this work`,
+ path: ['alternative_labels', i],
+ });
+ }
+ seenLabels.add(label);
+ });
});
export type Work = z.infer;
From 8bc25ce5b48b7ceb4f8b97b365093261856a3267 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20M=C3=A4hr?=
Date: Tue, 25 Aug 2026 14:58:53 +0200
Subject: [PATCH 45/45] feat(find): resolve a familiar citation to a canonical
reference (#90)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat(find): resolve a familiar citation to a canonical reference
Closes #90.
`/find/` turns a query a scholar already writes -- `Plato Republic 514a`
-- into the canonical reference for that passage: a permanent link, the
editions that carry it, and a citation to copy.
Resolution runs in two stages, because the two halves of the answer live
in two files. `/reg/works.json` names a work's preferred citation system
and nothing else: a work may declare `additional_systems`, and the
compiled `Work` record drops them (#91). Only
`/reg/work/{key}/aliases.json` lists every system a work carries
references under. So `interpret()` picks the work and splits off the
locator from the two small collections, and `resolveInIndex()` decides
system and identity once the caller has fetched that work's index. The
split also keeps the large fetch lazy: an alias index runs from 68 KB to
1.18 MB, and nothing requests one before a single work is identified.
Ambiguity is never resolved silently. A bare locator lists candidates
even when only one work matches today. Two works that share a label stay
two hits (ADR-0007), and one locator valid under two of a work's systems
stays a choice (ADR-0005). Fuzzy matching helps find a work and stops
there: the index lookup is exact, so no near miss can invent an identity
(ADR-0002).
The page is deliberately not a Starlight document. A sidebar, a table of
contents, and a second search box are noise beside one search field, so
it uses `RecordPage`, the minimal layout the `/id/` pages already share.
`RecordPage` gains an optional `jsonHref`: the finder has no single
JSON-LD sibling to advertise, and inventing one would misdescribe it.
`src/lib/find.ts` is pure and holds no DOM or network reference. That
keeps it testable under `node:test`, and keeps the browser bundle free
of the compiler and of Zod. `src/lib/citation.ts` is reused for the
citation rather than a second format; its parameters now name the fields
it actually reads, so a browser can pass the subset `/reg/works.json`
gives while the record pages pass full records unchanged.
The page stylesheet is `is:global` by necessity. Astro scopes a plain