Skip to content

chore(release): v0.1.0 — first citable baseline - #4

Open
maehr wants to merge 48 commits into
mainfrom
staging
Open

chore(release): v0.1.0 — first citable baseline#4
maehr wants to merge 48 commits into
mainfrom
staging

Conversation

@maehr

@maehr maehr commented Jun 7, 2026

Copy link
Copy Markdown
Member

Release v0.1.0 — first citable baseline

Publishes stagingmain as v0.1.0 (0.0.1 → 0.1.0: pre-1.0 breaking spec changes). This is the first tagged release; after merge, main is tagged v0.1.0, release.yml builds the dump and creates the GitHub Release, and Zenodo mints the concept DOI. Community announcement with the full story: discussion #31.

Tracked in #68, which carries the merge sequence and the pre-tag checklist.

What it ships

67,959 references across 12 works and 10 citation systems, all at status: draft — 68,005 records in total. That is up from ~39,200 when this PR was opened: the resolver review completed the New Testament (John only → 27 books) and the Tanakh (Genesis only → 39 books), so the baseline covers two complete biblical corpora rather than one book of each.

references.jsonl is 53.6 MB and the alias table is 12.9 MB; the built site is 204,361 pages plus 68,019 JSON endpoints.

Standard — seven ADRs adopted (decisions/)

ADR-0007 is purely additive and re-mints nothing: the field is optional, and no label has ever been an identity input. ADR-0005 and ADR-0006 both re-mint IRIs. Landing them before the first tag costs nothing — every record is draft under ADR-0004 and no identifier has ever been published. After the tag the same change would cost a documented migration against a baseline people may already cite. That timing argument is why they are in v0.1.0 rather than v0.2.0 (#68 §2).

Registry

The data/ submodule is pinned to textrefs/registry@237e39f, carrying:

  • the ADR-0006 reclassification of every mapping (registry#13);
  • the resolver review (registry#20): six resolver targets that did not address the cited passage removed, NA28, BHS, SuttaCentral and NINJAL added, and the New Testament and Tanakh completed. Closes registry#16–docs: add ORCID for Luz Christopher Seiberth #19.

Compiler and site

  • Locator variable mapping (feat(compile): map locator variables into a provider's own vocabulary (#71) #72, closes feat(compile): map locator variables into a provider's own vocabulary #71) — a resolver can translate a canonical locator value into a provider's own vocabulary (OSIS John → die-bibel.de's USFM JHN) instead of forcing a second citation system into existence.
  • Prototype-safe resolver expansion (fix: release hardening before v0.1.0 (#46, #47, #49, #50) #75) — every lookup on the path to a resolver URL is own-property only, so a locator value of toString skips the entry instead of expanding Object.prototype's method into a published URL.
  • Draft pages excluded from the sitemap (fix: release hardening before v0.1.0 (#46, #47, #49, #50) #75, closes Draft record pages are listed in the sitemap #47) — the third leg of ADR-0003's noindex rule. The predicate keys on record status, not route prefix, so it narrows as records are promoted. Incidentally cut the production build from 7m47s to 3m47s.
  • Non-SPDX licences fail the build (fix: release hardening before v0.1.0 (#46, #47, #49, #50) #75, closes Non-SPDX license values are silently dropped from the published JSON-LD #46) — previously dropped from the published record with only a console warning.
  • Retired mapping assertions no longer project onto Work (fix(compile): stop projecting retired mapping assertions onto Work (#45) #86, closes Mapping projection uses a denylist, so draft and deprecated mappings emit unqualified direct edges #45) — the direct alternateOf / isReferencedBy edges excluded only withdrawn and blocked, so a deprecated mapping still emitted a live, unqualified edge, against the standard's own definition of the three states for records that have left active use. The allowlist on draft that Mapping projection uses a denylist, so draft and deprecated mappings emit unqualified direct edges #45 also proposed was 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. No published byte changes today, since every record is draft, so this closes the hole before the first demotion rather than fixing a visible defect.
  • Collection endpoints for works and citation systems (feat(api): add works and systems collection endpoints (#83) #87, closes feat: collection endpoints for works and citation systems #83) — every operation in the 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(). /reg/works.json and /reg/systems.json now serve one @context and one @graph of records, sorted by key, in every status, and /reg/ advertises both through link rel="alternate". The contract also documents the five /dump/ artifacts the compiler has always written and Pages has always served but the OpenAPI never mentioned. RegistryObject stays unreferenced on purpose: it mirrors the exported Zod union in standard/schema/index.ts, so routing the collections through it would have widened both responses to a union a client must narrow. No registry data changes.
  • Resolve a reference by work and locator (feat(api): publish an alias index so clients resolve a locator without UUIDv5 (#84) #88, closes feat: resolve a reference by work and locator without computing its UUID #84) — the only public handle on a reference was its UUIDv5. A client that knew a passage as work, citation system and locator had to re-implement the seed ADR-0002 fixes before it could ask a single question; the compiler already built the answer but published it only as /cite/ meta-refresh HTML. Each work now carries a locator index at /reg/work/{key}/aliases.json, mapping locator to reference UUID grouped by citation system key, so a client resolves a passage in two fetches and parses JSON alone. The complete alias table ships at /dump/aliases.json and is registered in datapackage.json with a byte count and a sha256: hash — the artifact data/README.md and data/.gitignore have both named all along while no code wrote it. Values are bare UUIDs rather than IRIs, which halves the largest file, and the index carries no @context: it is a lookup table, not JSON-LD, and the contract says so. Largest emitted index is tanakh at 1.12 MB (566 KB gzipped), under the agreed 2 MB cap, so it is not paginated — paginating would force a client to fetch every page to find one locator. writeDump() split into pure halves so a JSON resource fits beside the four JSONL ones, and the alias keys are sorted so the body and its hash depend on registry content alone rather than on the order the compiler visited the work files. release.yml gained the matching glob: aliases.json matched neither existing pattern and fail_on_unmatched_files is set, so a tagged release would otherwise have shipped a descriptor listing a resource that was not in the release. All 67,959 references are reachable from their work index, and the four JSONL bodies are byte-identical to before. No registry data changes.
  • Works findable by abbreviation and translated title (feat(standard): name works by abbreviation and translated title (#85) #89, closes adr: alternate labels and abbreviations for works #85) — a Work carried exactly one name, and the registry browser built its filter key from preferred_label and key alone, so 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, and the same gap hit PI and LXX. The optional alternative_labels list is folded into the existing data-reg-item haystack, so the client filter needed no change, and it renders on the work record page. It flows into works.jsonl, /reg/works.json and /id/work/{key}.json untouched, because all three serialize the whole record rather than a field whitelist. Entries are unique within a work, never repeat the preferred_label, and an empty list is rejected — enforced on the authored YAML and again in the canonical Work schema, since that is the shape the standard publishes and a hand-built record must not be able to violate the normative sentence in §6. Two documents had promised this field under the name alt_labels since before the schema existed; the guide now documents the real one. No registry data changes, and no route is added. The matching data/AGENTS.md fix is open as registry#21 and is not in this pin.
  • A scholar can resolve a citation without reading the API docs (feat(find): resolve a familiar citation to a canonical reference (#90) #93, closes feat: resolve a familiar citation to a canonical reference at /find/ #90) — every route into the registry assumed you already knew something the citation does not carry. /reg/ filtered works by substring, and /cite/{work}/{locator} needed the work key and the locator grammar up front; neither separated a text from a passage, so Plato Republic 514a reached nothing. /find/ takes the string a scholar already writes and returns the reference: a permanent link, the editions that carry the passage, and a citation to paste. It is a static page that reads the same public endpoints any other client reads — feat: collection endpoints for works and citation systems #83's collections and feat(api): publish an alias index so clients resolve a locator without UUIDv5 (#84) #88's locator index — so it holds no private search index and no server. Resolution runs in two stages, and not by choice. /reg/works.json names a work's preferred citation system and drops additional_systems, so the collection cannot say which systems a work uses; only its alias index can. interpret() therefore picks the work and splits off the locator from the two small collections, and resolveInIndex() decides system and identity once that work's index is fetched. That gap is filed as feat: name every citation system a work uses in the Work record #91. The split also keeps the large fetch lazy: an index runs from 68 KB to 1.12 MB, and instrumenting window.fetch confirms none is requested until a single work is identified. Nothing is guessed, which is the part that matters for citation: a bare locator lists candidates even when only one work matches today, two works sharing a label stay two hits (ADR-0007), one locator valid under two of a work's systems stays a choice (ADR-0005), and fuzzy matching helps find a work but never reaches identity — the index lookup is exact, so no near miss can mint a reference (ADR-0002). Ranking is a fixed seven-tier ladder where only the leading tier decides, so a weak match cannot turn a good one into an ambiguity. The search logic sits in src/lib/find.ts, pure and free of DOM and network, which is what makes it testable under node:test and keeps Zod and the compiler out of the 10.7 KB browser bundle; src/lib/citation.ts is reused for the citation rather than a second format written, its parameters narrowed to the fields it actually reads. 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, which gains an optional jsonHref because the finder has no single JSON-LD sibling to advertise. Review caught two strings that spoke for the whole registry from a preferred-system-only check; both now say "main numbering". ADR-0007's deferred abbreviation question is filed as adr: ranked search over alternative labels, and duplicate-hit disambiguation #92, since ranked search is the trigger it named. No registry data changes.
  • Minimal static templates for reference pages (feat(site)!: minimal static templates for reference pages #30), draft flagging and noindex (feat(site): draft records flagged + noindexed (ADR-0003 follow-up) #26), CitationSystem pages listing works (fix(site): list works instead of references on CitationSystem pages (#55) #56, closes feat: CitationSystem pages should list works instead of references #55), inline whitespace fixes (fix(site): add explicit whitespace between inline elements #54, closes docs: Missing whitespace around inline elements on registry pages #53).

Documentation

  • Consistency audit (docs: resolve the consistency audit before v0.1.0 (#79) #80, resolves 40 of the 41 findings in docs: consistency audit — 41 findings across the standard, get-started, and mirrored root docs #79) — every documentation file checked against the schemas, the compiler, the published context, the OpenAPI contract and the compiled registry output. Four findings would otherwise have frozen wrong statements into a citable baseline: §13's flagship Work example was invalid against Work.safeParse for want of preferred_citation_system_key, the OpenAPI document described the advertised <link rel="alternate"> type as application/json where the HTML emits application/ld+json and omitted Work.creators, and §14 required BCP 47 validation that all three schema layers implemented as minLength: 2. That last one is now a shared LanguageTag (RFC 5646 langtag + privateuse), mirrored as an OpenAPI pattern.

    The audit's media-type finding touched only that prose. The response media types stayed application/json, and feat(api): add works and systems collection endpoints (#83) #87 confirmed that this is the correct declaration rather than an oversight: the site is static, so GitHub Pages derives every Content-Type from the file extension and discards the header the Astro APIRoute sets. On the live site /id/work/plato.republic.json returns application/json; charset=utf-8 and /dump/works.jsonl returns application/octet-stream. Declaring application/ld+json would have described a response no client receives. The contract now states the rule and tells clients to parse by documented shape rather than by response header.

  • The published contributing page described the wrong branching model (docs: resolve the consistency audit before v0.1.0 (#79) #80) — it told contributors to branch from and PR against main, and omitted the branching section entirely, while the infrastructure integrates on staging. Fixed in both halves of the mirrored pair, and the release checklist is resynced to the canonical 9 steps.

  • Related standards and systems (docs: resolve the consistency audit before v0.1.0 (#79) #80, closes docs: add more systems like IIIF #81) — the related-systems page framed every neighbouring standard as an identifier scheme. It is now grouped by the layer each system addresses, and adds IIIF Presentation API, W3C Web Annotation, BIBFRAME, IFLA LRM, RAMEN, and URN:NBN — the last of which Appendix B already listed and the page did not.

  • F36 is deliberately not in this release branch yet — the legal-reservation notice missing from the English association pages ships with the statutes batch, alongside the association's founding record.

Release plumbing

Spec version vs tag

The tag is v0.1.0; the specification stays at 0.1.0-draft with maturity: working-draft. These are deliberately different things — ADR-0004 and ADR-0006 both declined a spec bump under the working-draft clause, and 0.x releases stay working-draft by the maturity ladder. All registry data is draft and carries no persistence promise until expert review promotes it.

Verification

maehr and others added 2 commits June 7, 2026 16:05
Rewrite the Get Started body around the single-identity narrative
("a passage has one identity, the editions that carry it are many"),
keeping the Keep reading and Live examples lists as deep-link entry
points.

Document the branching model in CONTRIBUTING.md: main is the
production source and auto-deploys; staging is a long-lived batching
branch for content edits; publish by squash-merging staging into main.
Manual workflow_dispatch from staging is available as an escape-hatch
preview that temporarily replaces production.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 7, 2026 14:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the onboarding and contribution/deploy documentation to support a new long-lived staging branch workflow, while also adding an org-level .github repository as a git submodule.

Changes:

  • Rewrites the Get Started page copy around the “single passage identity, many editions” narrative.
  • Documents a main (production) + staging (batching) branching/publishing model in CONTRIBUTING.md.
  • Adds a github-profile submodule pointing at textrefs/.github.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
src/content/docs/get-started/index.md Reframes the Get Started explanation and adjusts link placement.
CONTRIBUTING.md Adds branching/deploy workflow guidance for main/staging and PR targeting.
.gitmodules Registers the new github-profile submodule for the org .github repo.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread CONTRIBUTING.md Outdated
Comment thread CONTRIBUTING.md
Fixes #3. Three buckets of false-positive errors:

- grep regex truncated URLs at `)`, mangling Wikipedia disambiguation
  titles. Allow `)` in URLs and strip only unbalanced trailing `)`.
- resolver templates with `{placeholder}` reached lychee verbatim.
  Filter them out.
- `data/package-lock.json` contributed bot-blocked opencollective URLs.
  Restrict grep to YAML/MD and add opencollective to lychee excludes.

Also bump the registry submodule for the Perseus license_url fix, and
mirror that URL change in the how-it-works example.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
maehr and others added 9 commits June 7, 2026 16:29
- npm update: astro 6.4.3 → 6.4.4 (patch)
- zod 3.25.76 → 4.4.3 (was already pulled in by astro/starlight as
  transitive at v4; align top-level so there's one resolved copy)

Zod 4 migration in schema + validator:
- z.string().url() → z.url() (Iri)
- z.ZodIssueCode.custom → 'custom' string literal (superRefine calls)
- validate-data reportIssue path type widened to PropertyKey[] to match
  zod 4's $ZodIssue['path']

GH Actions are already on current majors; no bumps needed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ssier

Vorstand minimum reduced from three to two persons (Statuten Art. 11)
in DE and EN; align contributing guides to frame all three review
tracks (technical, expert, board reservation) as first-class, matching
the governance regulation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
§1 previously said a conforming registry MAY record dereferenceable
locations. A bare identifier with no resolution path is of limited
practical use, so tighten to SHOULD to nudge implementers toward
providing a resolvable URL per reference.

Closes #7.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace §13's compound JSON-shaped wrapper with a single JSON-LD
document under @context + @graph. Each record carries full id, type,
and administrative metadata so a reader can copy the example out and
validate it directly against the published context and Zod schemas.

The CanonicalReference id is the deterministic UUID v5 produced by the
seed for `new-testament / bible-book-chapter-verse / John.3.16 / 1.0.0`,
verified locally against standard/schema/.

Closes #8.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…works

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
target_kind was an OPTIONAL human-readable scheme hint with no
normative weight; maintaining Appendix B's enumerated label list was
upkeep without payoff. Replace it with optional target.conforms_to —
an IRI (or array of IRIs) typed as dcterms:conformsTo in the v1
JSON-LD context — mirroring Linked Art's conforms_to pattern.

Spec, Appendix B, JSON-LD context, Zod schema, compile pipeline,
in-tree fixture, registry detail pages, and get-started prose all
migrated. The Astro mapping/work pages drop the scheme-label badge:
the identifier IRI is authoritative and self-describing.

The data/ submodule pointer moves to the matching textrefs/registry
commit (target_kind→conforms_to in every data/works/*.yaml).

See decisions/ADR-0001 for the rationale and alternatives considered.

BREAKING CHANGE: target.target_kind is removed; downstream consumers
that read it MUST migrate to target.conforms_to. Acceptable pre-v1.0.0.

Closes #6.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
* fix(ci): repair URL extraction in link-check workflow

Fixes #3. Three buckets of false-positive errors:

- grep regex truncated URLs at `)`, mangling Wikipedia disambiguation
  titles. Allow `)` in URLs and strip only unbalanced trailing `)`.
- resolver templates with `{placeholder}` reached lychee verbatim.
  Filter them out.
- `data/package-lock.json` contributed bot-blocked opencollective URLs.
  Restrict grep to YAML/MD and add opencollective to lychee excludes.

Also bump the registry submodule for the Perseus license_url fix, and
mirror that URL change in the how-it-works example.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore(deps): bump astro 6.4.4 and migrate to zod 4

- npm update: astro 6.4.3 → 6.4.4 (patch)
- zod 3.25.76 → 4.4.3 (was already pulled in by astro/starlight as
  transitive at v4; align top-level so there's one resolved copy)

Zod 4 migration in schema + validator:
- z.string().url() → z.url() (Iri)
- z.ZodIssueCode.custom → 'custom' string literal (superRefine calls)
- validate-data reportIssue path type widened to PropertyKey[] to match
  zod 4's $ZodIssue['path']

GH Actions are already on current majors; no bumps needed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs(association): sync statutes board size and review tracks with dossier

Vorstand minimum reduced from three to two persons (Statuten Art. 11)
in DE and EN; align contributing guides to frame all three review
tracks (technical, expert, board reservation) as first-class, matching
the governance regulation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs(spec): tighten dereferenceable-location guidance to should (#7)

§1 previously said a conforming registry MAY record dereferenceable
locations. A bare identifier with no resolution path is of limited
practical use, so tighten to SHOULD to nudge implementers toward
providing a resolvable URL per reference.

Closes #7.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs(spec): self-contained §13 example with @context (#8)

Replace §13's compound JSON-shaped wrapper with a single JSON-LD
document under @context + @graph. Each record carries full id, type,
and administrative metadata so a reader can copy the example out and
validate it directly against the published context and Zod schemas.

The CanonicalReference id is the deterministic UUID v5 produced by the
seed for `new-testament / bible-book-chapter-verse / John.3.16 / 1.0.0`,
verified locally against standard/schema/.

Closes #8.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore(data): bump submodule with second resolvers on single-resolver works

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(spec)!: replace target_kind with dcterms:conformsTo (#6)

target_kind was an OPTIONAL human-readable scheme hint with no
normative weight; maintaining Appendix B's enumerated label list was
upkeep without payoff. Replace it with optional target.conforms_to —
an IRI (or array of IRIs) typed as dcterms:conformsTo in the v1
JSON-LD context — mirroring Linked Art's conforms_to pattern.

Spec, Appendix B, JSON-LD context, Zod schema, compile pipeline,
in-tree fixture, registry detail pages, and get-started prose all
migrated. The Astro mapping/work pages drop the scheme-label badge:
the identifier IRI is authoritative and self-describing.

The data/ submodule pointer moves to the matching textrefs/registry
commit (target_kind→conforms_to in every data/works/*.yaml).

See decisions/ADR-0001 for the rationale and alternatives considered.

BREAKING CHANGE: target.target_kind is removed; downstream consumers
that read it MUST migrate to target.conforms_to. Acceptable pre-v1.0.0.

Closes #6.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Moritz Mähr <14755525+maehr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
@maehr maehr changed the title Bootstrap staging branch: revise Get Started + deploy flow Publish staging → main: spec fixes #6/#7/#8, CI/deps, governance, get-started rewrite Jun 9, 2026
@maehr
maehr temporarily deployed to github-pages June 9, 2026 22:15 — with GitHub Actions Inactive
* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Moritz Mähr <14755525+maehr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@maehr
maehr temporarily deployed to github-pages June 26, 2026 08:26 — with GitHub Actions Inactive
maehr and others added 8 commits July 5, 2026 21:13
… tuple (ADR-0002) (#21)

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* docs(spec): align quoted bible description with updated registry text

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Moritz Mähr <14755525+maehr@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ADR-0003) (#23)

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Moritz Mähr <14755525+maehr@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
All records demoted to draft per ADR-0003 (textrefs/registry#7); pin
returns to a registry main ancestor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…profiles (#24)

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 <noreply@anthropic.com>
…) (#25)

- #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 <noreply@anthropic.com>
…26)

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 <noreply@anthropic.com>
Catches staging up with dependabot PRs #16/#17/#18, which target main
and will close automatically at release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Moritz Mähr <14755525+maehr@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@maehr maehr changed the title Publish staging → main: spec fixes #6/#7/#8, CI/deps, governance, get-started rewrite Release v0.1.0: ADR-0002/0003, all data draft, spec fixes #6–#15, canonical ASCII forms Jul 5, 2026
maehr and others added 3 commits August 24, 2026 17:42
…) (#86)

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 <noreply@anthropic.com>
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
) (#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.
…#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.
* 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
<style> by rewriting selectors to `.foo:where(.astro-HASH)` and stamping
that hash onto template elements; the result card is built in the
browser, so every rule targeting it would silently do nothing. The sheet
ships with this page alone.

* docs(find): document the finder and put it on the way in

Refs #90.

Add `get-started/finding-references.md` as the second page in the
section, ahead of use cases. It covers the human path only: what to
type, the ranking ladder, the ambiguity rules, and what comes back.
`url-layout.md` already documents the machine path -- the two-request
`aliases.json` flow -- so that section is linked rather than restated,
and it now points back at the finder as its counterpart.

On the landing page, the second hero action becomes "Find a reference"
and the Registry card leads with finding. Browsing stays one click away
in both places.

* docs(find): stop claiming more than the finder knows

Review of #93 caught two strings that speak for the whole registry from
a partial check.

The bare-locator candidate list is filtered on each work's PREFERRED
citation system, because that is the only one `/reg/works.json` names
(#91). A work cited under a second system could accept the same locator
and never appear. "No text in the registry is numbered in a way that
would accept it" and "These are numbered in a way that would accept it"
both overstated that; each now says "main numbering". No work declares
`additional_systems` today, so neither line was false yet, but a page
built on refusing to guess should not guess in its own copy either.

The guide said the finder tells you the Iliad has 24 books. It quotes
the citation system's description and works nothing out; that entry
happens to mention the bound. Say so, since another system's
description may not name its own.

Copy only. No logic changed.

---------

Co-authored-by: Moritz Mähr <14755525+maehr@users.noreply.github.com>
Picks up four merged registry pull requests:

- textrefs/registry#21 — document `alternative_labels` in AGENTS.md
- textrefs/registry#29 — correct the Perseus editions, record two verified
  negatives (closes registry #22 and #24)
- textrefs/registry#30 — drop the three Wikisource resolver targets
- textrefs/registry#31 — seed alternative labels on all twelve works
  (closes registry #27)

`/find/` now resolves `NE 1094a1` to the Nicomachean Ethics through
`TIER.EXACT_ALTERNATIVE_LABEL`. Before this pin it resolved to the New
Testament through `TITLE_PREFIX`, which is the failure ADR-0007 was written to
close.

`npm run verify` and `npm run validate:data` both pass on the new pin: 68,005
records valid, 204,364 pages built, all internal links valid.


Claude-Session: https://claude.ai/code/session_01CD9MxbT2jSZmJyR9ywJGEs

Co-authored-by: Moritz Mähr <14755525+maehr@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* chore(data): bump the registry pointer to 7d109195

Picks up the second-wave works verified against registry #25:

- textrefs/registry#39 — Dante, Divina Commedia (14,233 references), with the
  verse-level Princeton resolver and a canto-level Dante Lab companion
- textrefs/registry#38 — Hume, Treatise and first Enquiry (1,468 references)
- textrefs/registry#40 — eight Nietzsche works (2,737 references)

Closes registry #34, #36 and #37. The registry goes from 12 works to 23, and
from 67,959 references to 86,397.

npm run verify passes on the new pin: 86,477 records valid, 259,811 pages built,
all internal links valid.

* chore(deps): safe update of all dependencies within their semver ranges

`npm update` only. package.json is untouched; the lockfile moves seven packages
to the newest version their declared range already allows:

- @astrojs/starlight     0.41.7 -> 0.41.9
- @commitlint/cli        21.2.1 -> 21.2.2
- @commitlint/config-conventional 21.2.0 -> 21.2.2
- astro                   7.2.1 -> 7.2.7
- mermaid                11.16.1 -> 11.17.2
- starlight-openapi      0.26.0 -> 0.26.1
- uuid                   14.0.1 -> 14.0.2

Deliberately not taken, because neither is a safe update:

- starlight-blog 0.28.0 -> 0.29.0 is outside the declared range.
- typescript 6.0.3 -> 7.0.2 is a major.

Three high-severity advisories remain and are NOT fixed here. All three are the
same chain — form-data (CRLF injection) via httpsnippet via starlight-openapi —
and npm's only offered remedy is a DOWNGRADE of starlight-openapi to 0.24.0,
which it flags isSemVerMajor. `npm audit fix --force` would move the tree
backwards, so it was not run. starlight-openapi is a devDependency used to
render the API docs at build time; nothing in that chain reaches a published
page.

npm run verify passes: 86,477 records valid, 259,811 pages built, all internal
links valid.

---------

Co-authored-by: Moritz Mähr <14755525+maehr@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants