Skip to content

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

Open
maehr wants to merge 66 commits into
mainfrom
staging
Open

chore(release): v0.1.0 — first citable baseline#4
maehr wants to merge 66 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.

Content complete; one mechanical step left. This PR is stagingmain,
so it picks up whatever staging holds at merge time. Every bullet below has
landed; none carries a pending marker. Nothing in #68 §0 is outstanding
any more.
The association's founding record landed with #124 (819fcb9) and
its announcement with #142.

staging is now ba908c7 (#143), eleven commits past the 56cab1b
this description named before — the founding documents, the announcement,
the published ontology, and seven site, compiler and standard fixes. All are
described below.

The only step left: dispatch github-pages on ba908c7, then approve and
merge.
Nothing further is queued for staging; if it moves again, repeat
the dispatch on the new tip first.

What it ships

86,397 references across 23 works and 13 citation systems, all at status: draft — 86,477 records in total, plus 172,838 aliases.

That is up from ~39,200 when this PR was opened, in two steps. The resolver review completed the New Testament (John only → 27 books) and the Tanakh (Genesis only → 39 books), taking it to 67,959 across 12 works, so the baseline covers two complete biblical corpora rather than one book of each. The second-wave works (#96) then added Dante's Divina Commedia (14,233 lines, the canonical count), Hume's Treatise and first Enquiry, and eight Nietzsche works — 23 works and 86,397 references.

references.jsonl is 69 MB (72,591,804 bytes) and the alias table is 17 MB (17,521,319 bytes); the built site is 259,811 pages. Both are far inside GitHub's 2 GB asset limit, but neither is a trivial upload — see Verification.

Standard — seven ADRs adopted (decisions/)

Their follow-up checklists were refreshed before the tag (#110): twelve boxes were unchecked for work that had shipped, ADR-0006 still read Proposed although it is implemented, and seven deferred items existed only inside decisions/ — each now carries an issue (#103#109). ADR-0006's last substantive follow-up also landed: specification §10 now states that an isReferencedBy target stays a lookup alias rather than leaving it to a code comment. All eight remaining unchecked boxes carry a tracker.

The ontology is published (2d71119, #127, closes #108) — ADR-0006's last follow-up action, and the one published claim in the standard that nothing backed. The v1 context has used tr: terms since the bootstrap, and every one of them dereferenced to nothing. All 17 are now defined in src/lib/ontology.ts and served at /ontology/ (HTML) and /ontology.json (JSON-LD). Where an established term already fitted it is reused rather than minted, as #108 asked: key is dcterms:identifier and locator is skos:notation. The published context stays inside JSON-LD 1.0, so every processor can still read it — which is why RDF reification of MappingAssertion is not here: it needs "@version": 1.1 and a term-scoped context, which a 1.0 processor rejects outright, and it needs a decision record of its own (#135). jsonld.expand under both processing modes expands a record identically, and jsonld.toRDF on the built dist/ontology.json types every term in the default graph.

license_url publishes as dcterms:rights, not dcterms:license (c360c02, #137, closes #129) — a normative correction to §9, and the reason it is in this release rather than after it: the tag freezes the specification text, and §9 described a field that no record in the registry uses. license and license_url both expanded to an IRI-typed dcterms:license, so the 50,047 targets carrying both published two IRIs on one predicate with no way for a consumer to rank them. §9 called license_url the licence text for licences outside the SPDX list; in fact every value is a rights statement about the target — provider terms pages (41,193), an imprint (31,167), terms pages (23,213 and 7,954) — so the predicate was wrong on all 104,427 targets that carry the field, not only on the overlap. DCMI recommends referring to a rights statement by URI, dcterms:rights declares no domain to violate, and license keeps dcterms:license, so the two now say different things and no data is dropped. standard/schema/context.test.ts fails whenever any two context terms share a predicate, which is the shape of this bug rather than this instance of it. No ADR: the documented meaning matched no record, so this is a correction and not a choice between defensible options.

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@7d10919, carrying:

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 references were reachable from their work index at the time (67,959; now 86,397), 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 landed as registry#21 and is 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.
  • A deprecated record renders as a tombstone (fix: link-check extraction, deprecated tombstones, orphaned component, docs label (#3, #99, #48) #101, landed — closes Decide what a deprecated record page shows: ADR-0004 calls it a tombstone, the templates do not #99)deprecated is one of the three tombstone statuses in the model, and ADR-0004, specification.md:284, the Status enum comment and RETIRED_STATUSES in the compiler all say so, yet both record templates tested withdrawn and blocked only. A deprecated record therefore rendered with no banner at all, presented exactly like an active one, and dropped its superseded_by link even though the compiler accepts the field on that status and the .json sibling publishes it. A reader following the identifier saw no successor and no notice. The decision behind the fix: the three statuses stay one tombstone category with one presentation, and the user-facing copy carries the difference. They differ in reach, not in kind — a deprecated record is retained and still resolves, while a withdrawn or blocked record is additionally excluded from Work mapping projections — so reusing the withdrawn wording would have stated something false about a record that still resolves, and giving deprecated its own banner class would have split a category the model keeps whole. Latent, not live: no record in the registry is deprecated today, so nothing was mis-rendered, and the cost would have landed the first time a promoted record was corrected. No spec change is needed; the templates were the half that disagreed. The copy still lives in two templates, and Single-source the draft and tombstone banner copy #100 single-sources it after the tag. No registry data changes.
  • The orphaned CitationBox.astro is gone (fix: link-check extraction, deprecated tombstones, orphaned component, docs label (#3, #99, #48) #101, landed — closes Delete the orphaned CitationBox.astro #48) — no consumers since feat(site)!: minimal static templates for reference pages #30 inlined Chicago and COinS rendering against src/lib/citation.ts. Identical dist/ output.
  • The registry compiles once per build, not twice (4d1c708, perf(site): compile the registry once per build, not twice #139, closes Every build compiles the registry twice #131) — Astro loads astro.config.mjs in a module graph of its own, separate from the one the pages render in, so src/lib/registry.ts was instantiated twice in one process and its module-level memo was never shared. Compiling the real registry costs ~2.6s and materialises 86,397 references, so every build and every dev start paid that twice, in time and in memory. Building the sitemap predicate lazily — what the issue proposed — does not help: two module graphs never share a module variable, whenever the first call happens; laziness moves the second compile rather than removing it. The memo now sits on globalThis under Symbol.for('textrefs.compiledRegistry'), the one scope both graphs agree on. Lifetime is unchanged (the process, exactly as before), so no new staleness. Instrumented builds confirm 2 calls → 1, with 259,812 pages both times. Two module graphs cannot be simulated under node:test, so nothing asserts the cross-graph sharing directly; what is tested is the footgun the change creates — the fixture short-circuit must stay above the cache, and src/lib/registry.test.ts fails when the two lines are swapped, its runtime jumping from 0.3ms to 871ms as the real compile happens.
  • Redirects and untranslated fallbacks leave the sitemap (257f17e, fix(site): keep redirects and untranslated fallbacks out of the sitemap #136, closes Draft /cite/ alias pages are indexed and listed in the sitemap #128) — three rules now decide what the sitemap advertises, and only the first is about noindex, so src/lib/noindex.ts becomes src/lib/sitemap.ts. ADR-0003's draft-record rule is unchanged and still derived from record status, so it narrows on its own as records reach active. A /cite/ alias is a redirect to a canonical /id/ URL, and a search engine treats a redirect in a sitemap as an error; aliases are now excluded by route whatever their record's status, which drops 172,794 entries to 0. A /de/ page with no source file is a Starlight fallback that serves an English body under <html lang="de"> with no robots meta, so advertising it offers a crawler mislabelled duplicate content; the set is read from src/content/docs/de/ and grows on its own as pages are translated, taking /de/ entries from 35 to 5 — exactly the Association pages that AGENTS.md says are bilingual. The robots meta tag still goes on a draft alias page, which ADR-0003 requires independently of the sitemap. Seven new tests in src/lib/sitemap.test.ts.
  • A whole-query label beats a split match in /find/ (d9239bd, fix(site): let a whole-query label beat a split match in /find/ #138) — interpret ranked the whole query only when the split found nothing, so any split hit permanently hid a better whole-query match. Nietzsche's registered labels include MA, MA I, MA II/1 and MA II/2; splitting MA II/1 leaves MA, the label of a different work, so the query landed on that work and reported II/1 as an invalid passage. The whole query is now ranked whenever a locator was split off, and wins at an equal or better tier: a match that explains the entire input beats one that had to discard a token. A spelling guess still never displaces a real hit — a fuzzy whole-query match is offered only when the split found nothing at all, which is the previous behaviour exactly. A sweep over the real registry takes every label of every work to its own work, 114/116 → 116/116, and the "label + locator" sweep stays 93/93, so no query that already worked was disturbed.
  • The resolver-target licence and rights render correctly (ba908c7, fix(site): render the resolver-target licence and rights correctly #143, closes Resolver-target licence tag shows a full URL instead of an SPDX identifier #133 and Reference pages never render license_url, hiding rights for 54,380 targets #144) — two defects in one block of the reference-page template, both of which the tag would otherwise freeze into the first citable baseline. The licence chip showed a URL: specification §9 requires a published license to be the canonical SPDX IRI so dcterms:license has a single IRI-typed range, and the chip — written when license held a bare identifier — put that IRI in a tag the width of a word. It now reads CC-BY-SA-3.0 and links to the SPDX page. SPDX_LICENSE_BASE is exported from scripts/compile.ts, because the compiler mints the IRI and spdxLicenseLabel reverses it, so the two cannot drift; a value without the canonical prefix is returned unchanged, which the compiler cannot emit but a template must not turn into an empty chip. The rights statement was never rendered at all: fix(standard): publish license_url as dcterms:rights, not dcterms:license #137 split license (dcterms:license) from license_url (dcterms:rights) and the page kept only the first half. Of 172,376 resolver targets, 27,803 carry an SPDX licence alone, 50,047 carry both, and 54,380 carry a rights statement and no licence — two providers, die-bibel.de/impressum (31,167) and sefaria.org/terms (23,213), both with access: open. So the NA28 and BHS rows showed one open chip and nothing else, which a reader can fairly take as "freely usable" when the registry recorded the opposite. license_url now renders as a Rights chip beside the licence chip rather than instead of it, since suppressing one would re-flatten the distinction fix(standard): publish license_url as dcterms:rights, not dcterms:license #137 had just established in the data model. The label is Rights because it is the one word true of all six license_url values in the registry without inspecting any of them and it matches the predicate — an Impressum is not "terms", and a hostname would repeat the provider link in the same row; rel="license" stays on the SPDX link alone. Each chip carries an aria-label naming its provider, because a page holds several targets and a screen reader would otherwise announce several identical Rights links. The fixture registry gained one licensed target and one rights-only target: it had neither, so no fixture-backed build rendered either chip and npm run verify:fast could not have caught either bug. The published JSON-LD is unchanged — verified against the built dump. The registry-side half is license_url on SuttaCentral and NINJAL targets is a CC deed, not a provider rights statement registry#42, deferred past the tag.
  • 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).

API audit follow-ups (#111)

An audit of the live site against the ReSed API-design criteria found ten items (#111). Five
landed on staging on 2026-08-31, in three PRs. Two were published claims the site did not
meet
, which is why they are in this release rather than after it: a tagged baseline should not
name a URL that returns 404.

Four routes are added: /api/openapi.yaml, /dump/, /robots.txt, and
/schemas/v1/textrefs.schema.json. No registry data changes, and no record IRI moves.

The five findings that stay open are tracked in #111: G2 (/cite/ and /find/ absent from the
contract), G6 (no stated HTTP versioning policy), G7 (the no-query-parameter choice is unstated),
G9 (the German hreflang alternate on record pages 404s), and G10 (the production 404 page
carries a draft banner). None is a false published claim, and each needs a decision before code.

Two checks belong after the deploy, because a static host decides them: curl -sI https://textrefs.org/api/openapi.yaml must return 200 with text/yaml — the value the contract
records, confirmed against two live GitHub Pages deployments rather than assumed — and
curl -sI https://textrefs.org/id/work/does-not-exist.json must return 404 with text/html.

Review follow-ups (#120, #121)

A review of this PR found six defects across the finder, the contract, the docs, the build and the
minimal pages. #121 fixed them and landed on staging as
56cab1b, which closes #120. Every finding was reproduced
before it was fixed.

  • /find/ matched a fragment of a name — tier 6 compared the query against one joined string,
    so includes accepted any fragment of any name: ant sits inside Dante, and interpret
    carried that fragment through to locator resolution. The haystack is now a set of normalized
    tokens, and membership is exact.
  • /find/ resolved a typo straight to a passageinterpret treated every tier alike, so a
    single fuzzy hit returned resolvable. The page tells the reader "Nothing is guessed", so that
    claim was false. A fuzzy leading tier now returns work-matches, which already meant "never
    resolved further", and the page renders it as "Did you mean …?". Republik still finds the
    Republic; it asks first.
  • The contract rejected data the site publishesIri declared format: uri, and RFC 3986
    forbids the non-ASCII characters that registered resolver targets carry. iri is a defined
    format in the JSON Schema 2020-12 vocabulary that OpenAPI 3.1 uses, and
    standard/schema/json-schema.ts already applied this exact correction to the generated schema;
    the OpenAPI document was the last place still saying uri. New api/openapi.test.ts validates
    real non-ASCII targets against the parsed contract, because nothing else in the build checked
    data against it.
  • Permanence was promised for draft records — every record in this release is draft, which
    carries no persistence promise under ADR-0004, and the get-started pages promised permanence
    anyway. finding-references.md, how-it-works.md, url-layout.md, get-started/index.md and
    the /reg/ front page now reserve "permanent" for an active record. The normative pages
    already did.
  • /dump/ recomputed the whole dump on every build — see the corrected bullet above. The new
    DUMP_MANIFEST carries the file list with no body, and a test locks it to what dumpResources
    produces, so the page and the descriptor still cannot disagree about which files exist. The
    verify snippet on the same page printed no hash: grep -A2 stops five lines short of the field
    in the descriptor order. It uses jq now.
  • The minimal record pages ran a second visual systemrecord.css invented a palette and
    used system-ui, against AGENTS.md. Colours already flowed through custom properties, so only
    the token block changed. The file is served unbundled from public/ and cannot import
    brand.css, so both files now carry a comment naming the other.

Fuzzy matching is kept, but stripped of authority: a near miss offers, and never applies.
#120 asked whether the tier earns its place; cutting it is a small follow-up. The STE rewrite of
the get-started introduction is the one acceptance criterion #121 does not meet; it is a style
question, not a false published claim, so it is deferred to #122 and does not block the tag. #120
is closed.

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.

  • The tombstone projection rule is stated correctly (990fc4f, docs(standard): state the tombstone projection rule correctly #140) — specification §11 and two code comments said a deprecated record is retained and still resolves "while a withdrawn or blocked record is additionally excluded from Work mapping projections". The compiler excludes all three: RETIRED_STATUSES is {deprecated, withdrawn, blocked} and the projection loop skips every one. §6 already said this correctly, so the specification contradicted itself, and both comments cited the constant that disagreed with them. The two halves of the sentence were attached to the wrong statuses: the projection rule covers all three, and the additional rule for withdrawn and blocked is the dependency rule — a live record may not depend on them (TOMBSTONE_STATUSES). §11 now says that, in all three places. Prose only: scripts/compile.test.ts has carried deprecated mappings are excluded from the projection throughout, so the behaviour was correct and covered; the comments now point at that test rather than restating it. The user-visible tombstone banner is unchanged and was never wrong.

  • F36 has shipped (819fcb9, docs(association): finalise the founding documents and limit Board members to expense reimbursement (#123) #124) — the :::caution[Non-binding translation] aside is on all four English association pages: statutes.md, mission.md, governance.md and expenses.md. It arrived with the statutes batch, as this description predicted; see the Association section below.

  • The repository's agent instructions load at all (41f2d31, chore(agents): track CLAUDE.md and document the GitHub workflow #141, closes feat: close the AGENTS.md and CI hardening gaps #125 A1 and A3) — CLAUDE.md was untracked, and Claude Code reads CLAUDE.md and ignores AGENTS.md, so a fresh clone loaded no agent instructions. It is tracked now, holding the single line @AGENTS.md. AGENTS.md also gained the five GitHub workflow rules that governed this release but lived only in CONTRIBUTING.md and in habit — sign every commit, branch from staging for content, branch from main for infrastructure, open every PR from the maehr fork, squash-merge only — plus the warning that the Update-branch button and --allow-update-branch both produce an unsigned commit, which the main ruleset's required_signatures then rejects. npm run check:links is documented; package.json had defined it and no instruction file named it. Two Markdown files outside the site build; no source file, workflow or repository setting changes.

Association

The last content item in #68 §0, and the reason this PR waited: a first citable baseline should
freeze the association as a record, not as a description. Both halves are now on staging.

The German texts are legally binding; the English versions are convenience translations and now
say so on every page (F36, above).

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 7 commits September 2, 2026 22:40
`interpret` ranked the whole query only when the split found nothing, so
any split hit permanently hid a better whole-query match.

Nietzsche's registered labels include `MA`, `MA I`, `MA II/1` and
`MA II/2`. Splitting `MA II/1` leaves `MA`, which is the label of a
different work, so the query landed on that work and reported `II/1` as
an invalid passage. Two works could not be reached by their own labels.

Rank the whole query whenever a locator was split off, and let it win at
an equal or better tier. A match that explains the entire input beats
one that had to discard a token. A spelling guess still never displaces
a real hit: it is offered only when the split found nothing, exactly as
before.

Closes #130


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

Co-authored-by: Moritz Mähr <14755525+maehr@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Specification §11 and two matching comments claimed that a `withdrawn`
or `blocked` record is additionally excluded from `Work` mapping
projections. The compiler excludes all three retired statuses:
`RETIRED_STATUSES` is `{deprecated, withdrawn, blocked}` and the
projection skips every one of them.

The two halves of the sentence were attached to the wrong statuses. The
projection rule covers all three. The additional rule for `withdrawn`
and `blocked` is a different one: a live record may not depend on them,
which the compiler enforces through `TOMBSTONE_STATUSES`.

§6 already stated the projection rule correctly, so the specification
contradicted itself. The comments cited `RETIRED_STATUSES` as agreeing
with them while that constant said the opposite.

No test is added. `scripts/compile.test.ts` already asserts that a
deprecated mapping leaves the projection, so the behaviour was correct
and covered; only the prose was wrong. The comments now point at it.

Closes #132


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

Co-authored-by: Moritz Mähr <14755525+maehr@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ap (#136)

* fix(site): exclude draft /cite/ alias pages from the sitemap

ADR-0003 (as amended by ADR-0004) says a draft record page is `noindex`,
excluded from search, and excluded from the sitemap. The rule covered
`/id/` record pages and the `/reg/` browsers. It did not cover `/cite/`.

Each alias renders its own redirect page, so a draft record reached the
sitemap through its aliases even when its own page was filtered out. The
real registry builds 172,794 alias pages, and every record is still
`draft` under ADR-0004, so all of them were listed.

Derive the alias status from the record it redirects to. Add the
`robots` meta tag to the redirect page as well, so all three legs of the
rule now apply. An alias holding an external IRI renders no page and
needs no entry.

Closes #128

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HLhfpsrWxq56jnZr1gD8B

* fix(site): keep redirects and untranslated fallbacks out of the sitemap

A `/cite/` alias redirects to a canonical `/id/` URL. A redirect is not
a destination, and a search engine treats one in a sitemap as an error.
Exclude every alias, whatever the status of the record behind it. This
is a rule about the route, so it replaces the status-derived alias rule
rather than extending it.

A locale page with no source file of its own is a Starlight fallback: it
carries `<html lang="de">` over an English body. Advertising it offers a
crawler mislabelled duplicate content. Only five of the 35 German URLs
have a German source file today.

Rename `noindex.ts` to `sitemap.ts`. The module now decides what the
sitemap advertises, and two of its three rules are not about `noindex`.
Keep the `robots` meta tag on a draft alias page, which ADR-0003 still
requires.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HLhfpsrWxq56jnZr1gD8B

---------

Co-authored-by: Moritz Mähr <14755525+maehr@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Astro loads `astro.config.mjs` in a module graph of its own, separate
from the one the pages render in. `src/lib/registry.ts` is therefore
instantiated twice in a single build, and the module-level memo is not
shared between the two copies. An instrumented `astro build` called
`compileRegistry` twice.

Deferring the first call does not help, because the two graphs never
share a module variable whenever it runs. Memoise on `globalThis`
instead, under a registered symbol, which is the one scope both graphs
agree on. An instrumented build now calls `compileRegistry` once.

Compiling the real registry takes about 2.6s and materialises 86397
references, so the second pass cost that time and a second copy in
memory on every build and every `dev` start.

Lifetime is unchanged: the process, exactly as the module memo was. A
`dev` server still needs a restart after the `data/` submodule changes.

Closes #131


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

Co-authored-by: Moritz Mähr <14755525+maehr@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(standard): publish the TextRefs ontology

* fix(standard): serve the ontology as RDF and scope the context check

Keep every ontology triple in the default graph. A node object that
carries both `@id` and `@graph` declares a named graph, so a consumer
that parsed the document into one graph found no terms at all.

Add `/ontology.json`. GitHub Pages cannot negotiate content, so the
ontology needs an explicit JSON-LD sibling like every record page.

Check each emitted key against the scope that defines it. Flattening the
scoped `target` context accepted `identifier` on any record, where
expansion silently drops it.

Guard the `relation` enum against a missing context term, and assert the
ontology version against `package.json`. Render domain and range in the
HTML view. Add `rdfs:isDefinedBy` and a licence to the ontology.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HLhfpsrWxq56jnZr1gD8B

* refactor(standard): keep the ontology work inside JSON-LD 1.0 and ADR-0006

Publish the `tr:` terms rather than replace them. ADR-0006's follow-up
asks for an ontology stub because `tr:Work`, `tr:relation`, and
`tr:locator` do not dereference. Defining them discharges that debt.

Drop the RDF reification of `MappingAssertion`. It needed `@version:
1.1` and a term-scoped context, which make a JSON-LD 1.0 processor
reject the whole context. It also needed a new ADR, so it moves to a
follow-up issue and `tr:MappingAssertion` stays.

Keep the two reuses issue #108 asks for: `key` is a `dcterms:identifier`
and `locator` is a `skos:notation`. Both work under JSON-LD 1.0. The
published context now differs from `staging` by two lines.

Revert `scripts/validate-data.ts` and `specification.md`. With no
scoped context, the plain term check is correct again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HLhfpsrWxq56jnZr1gD8B

---------

Co-authored-by: Moritz Mähr <14755525+maehr@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ense (#137)

`license` and `license_url` both expanded to an IRI-typed
`dcterms:license`, so a resolver target that carried both published two
IRIs on one predicate and gave a consumer no way to rank them. 50,047
targets carry both.

The registry shows what `license_url` really holds. Not one value is a
licence document: 41,193 point at the Perseus terms page, 31,167 at the
Deutsche Bibelgesellschaft imprint, 23,213 at the Sefaria terms, 7,954
at the SBLGNT licensing page, and 900 restate the SPDX licence already
in `license`. Every one is a rights statement about the target.

Publish `license_url` as `dcterms:rights`, which DCMI recommends be
referred to by URI. Rewrite Specification §9, which described a field
that no record uses: `license_url` is not a fallback `license`, so a
target may hold both.

Guard the invariant. A test now fails when any two context terms share a
predicate, which is the shape of this bug.

Closes #129


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

Co-authored-by: Moritz Mähr <14755525+maehr@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Claude Code reads CLAUDE.md and ignores AGENTS.md. The bridge file was
untracked, so a fresh clone loaded no agent instructions. Track it.

Add a GitHub workflow section to AGENTS.md. An agent reads AGENTS.md
first, and the file never named the signed commit, the branch model, the
fork, or the squash merge. CONTRIBUTING.md holds the full model, so link
it from the new section and from Pointers.

Also document npm run check:links, which package.json defines and no
instruction file mentioned.

Refs #125


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

Co-authored-by: Moritz Mähr <14755525+maehr@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
maehr and others added 2 commits September 3, 2026 11:15
The association was founded on 2 September 2026. Publish the announcement, because the founding documents landed without a public notice.

State the assured tax exemption of 27 August 2026 and its condition. State that donations become deductible only after the legally binding decision. An assurance is not a rechtskraeftige Verfuegung.

Closes the last content item before the v0.1.0 tag (#68).

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

Two defects in one block of the reference-page template.

The licence chip showed the canonical SPDX IRI where a reader expects an identifier. Derive the label from the IRI and keep the IRI as the link target. Export SPDX_LICENSE_BASE from the compiler, so the minting and the reversing side cannot drift.

The rights statement beside it was never rendered. #129 split license (dcterms:license) from license_url (dcterms:rights) and the page kept only the first half. 54,380 targets carry a rights statement and no SPDX licence, across two providers that both carry access: open, so a reader saw one open chip and nothing else. Render license_url as a Rights chip, beside the licence chip and not instead of it.

Give the fixture registry one licensed target and one rights-only target. Without them no fixture-backed build renders either chip.

The published JSON-LD is unchanged.

Closes #133
Closes #144

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

maehr commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

@julsraemy @flicksolutions @stephenhart8 — this is ready for your review.

What we are asking for

Please review the decisions and the specification, not the code. The implementation has had its own pass: CI is green on every commit, a review round in #120 found six defects that #121 fixed, and the code sections in the description above are there as evidence, not as a reading list.

What we cannot check ourselves is whether the model is right. Concretely:

  • The seven ADRs in decisions/. ADR-0004 collapses the lifecycle to draftactive. ADR-0005 gives every work one preferred citation system, which decides what a bare /cite/ alias means. ADR-0006 replaces the SKOS mapping relations with prov:alternateOf and dcterms:isReferencedBy, chosen by what the target is rather than by confidence. ADR-0007 adds alternative_labels. ADR-0001 came out of Julien's pre-release review and is the one already shaped by outside input.
  • standard/specification.md — in particular §6 (Work), §9 (resolver targets, license and license_url), §11 (tombstones), the §13 worked example, and §14 (validation).
  • The published vocabulary/ontology/ and public/contexts/v1.jsonld. All 17 tr: terms are defined for the first time, and terms that had an established equivalent were remapped to it: key is dcterms:identifier, locator is skos:notation.
  • api/openapi.yaml — whether the contract is one a client would actually want to call.

Reading the rendered site is a fair substitute for reading the diff: /standard/, /api/, /find/ and any record page under /reg/.

Why the timing matters

v0.1.0 is the first tag, so nothing here is a migration yet. Every record is status: draft and carries no persistence promise, and no identifier has ever been published. A model change costs nothing today and costs a documented migration after the tag — that argument is why ADR-0005 and ADR-0006 are in this release rather than the next one, and it applies equally to anything you find now.

So a "this is wrong" is more useful than a "this is fine", and a question about something that merely reads oddly is worth raising.

State

Content-complete. staging is at ba908c7, all four checks pass, and the full gate against the real registry reports 0 type errors, 137/137 tests, 259,813 pages and every internal link valid. What remains is a Pages dispatch and one maintainer approval.

The release ships 86,397 references across 23 works and 13 citation systems. The full detail is in the description above; the community-facing summary is discussion #31.

No deadline is being imposed here. Say if you want more time, and the tag waits.

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