Conversation
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>
There was a problem hiding this comment.
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 inCONTRIBUTING.md. - Adds a
github-profilesubmodule pointing attextrefs/.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.
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>
- 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>
* 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>
… 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>
Co-authored-by: Moritz Mähr <14755525+maehr@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
`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>
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>
|
@julsraemy @flicksolutions @stephenhart8 — this is ready for your review. What we are asking forPlease 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:
Reading the rendered site is a fair substitute for reading the diff: Why the timing matters
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. StateContent-complete. 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. |
Release v0.1.0 — first citable baseline
Publishes
staging→mainas v0.1.0 (0.0.1 → 0.1.0: pre-1.0 breaking spec changes). This is the first tagged release; after merge,mainis taggedv0.1.0,release.ymlbuilds 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
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.jsonlis 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/)dcterms:conformsToreplacestarget_kind(target_kind: prefer a dereferenceable IRI +dct:conformsToover an enumerated scheme list #6) — mapping targets drop the curated scheme-label column; an optionalconforms_toIRI carries the conformance claim, following Linked Art practice.CanonicalReferenceUUIDs seed from the semantic identity tuple(work_key, citation_system_key, locator)— computable offline;normalization_versionremoved from the data model (adr: purpose of deterministic UUIDs (compiler reproducibility vs. offline minting) #15).draftlifecycle tier belowcandidate. The identifier-persistence promise attaches at promotion (expert review), not publication. Drafts may be corrected or retracted without tombstones (adr: review lifecycle and ephemeral draft state (retractable pre-promotion identity) #22). Superseded by ADR-0004.draft→active— promotion grants recommendation and identifier permanence in a single expert-review gate, since nothing ever treatedcandidatedifferently fromactiveexcept the recommendation.Worknames a preferred citation system, which mints the bare/cite/{work}/{locator}alias; fallback systems get qualified/cite/{work}/{system}/{locator}aliases. Closes adr: Introduce a preferred Citation system #60.prov:alternateOf(another entity denoting the same work) anddcterms:isReferencedBy(a document about the work), chosen by what the target is rather than by confidence.closeMatchis removed outright. Closes standard: owl:sameAs instead of skos:exactMatch #58 and standard: replace skos:closeMatch to link to URL pages #59.WorkMAY carryalternative_labels, a flat list of the other names a scholar searches by — abbreviations (NE,LXX,PI), Latin or Greek titles, and translated titles — published asskos:altLabel. Identity-neutral: no label is a UUID seed input, so a label change never moves an identifier. Two different works MAY claim the same label (Ethicsfits Aristotle and Spinoza), which the compiler allows rather than treating as an error. Closes adr: alternate labels and abbreviations for works #85.Their follow-up checklists were refreshed before the tag (#110): twelve boxes were unchecked for work that had shipped, ADR-0006 still read
Proposedalthough it is implemented, and seven deferred items existed only insidedecisions/— each now carries an issue (#103–#109). ADR-0006's last substantive follow-up also landed: specification §10 now states that anisReferencedBytarget 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 usedtr:terms since the bootstrap, and every one of them dereferenced to nothing. All 17 are now defined insrc/lib/ontology.tsand served at/ontology/(HTML) and/ontology.json(JSON-LD). Where an established term already fitted it is reused rather than minted, as #108 asked:keyisdcterms:identifierandlocatorisskos:notation. The published context stays inside JSON-LD 1.0, so every processor can still read it — which is why RDF reification ofMappingAssertionis not here: it needs"@version": 1.1and a term-scoped context, which a 1.0 processor rejects outright, and it needs a decision record of its own (#135).jsonld.expandunder both processing modes expands a record identically, andjsonld.toRDFon the builtdist/ontology.jsontypes every term in the default graph.license_urlpublishes asdcterms:rights, notdcterms: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.licenseandlicense_urlboth expanded to an IRI-typeddcterms:license, so the 50,047 targets carrying both published two IRIs on one predicate with no way for a consumer to rank them. §9 calledlicense_urlthe 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:rightsdeclares no domain to violate, andlicensekeepsdcterms:license, so the two now say different things and no data is dropped.standard/schema/context.test.tsfails 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
draftunder 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 totextrefs/registry@7d10919, carrying:alternative_labelsseeded on all twelve works then registered, the Perseus editions on the Republic and the Nicomachean Ethics corrected, the Greek OCT resolvers added and the three Wikisource targets dropped (chore(data): bump the registry pointer to 455bb27f #95, registry#21/feat(deps)!: Astro 7 / Starlight 0.41 + all deps to latest (#20) #29/feat(site)!: minimal static templates for reference pages #30/v0.1.0 status: everything is a draft (on purpose) — release staged, review invited #31). This is what makes ADR-0007'sEXACT_ALTERNATIVE_LABELtier live:/find/resolvesNE 1094a1to the Nicomachean Ethics where before the pin it reached the New Testament, becauseneis a literal prefix ofnew testament;dante-cantica-canto-verse,hume-section-paragraph,hume-book-part-section-paragraph). Every extent was harvested from the provider and checked against an external total rather than typed. One conscious exception: the Dante Lab resolver is plaintext HTTP, becausedantelab.dartmouth.edupresents a certificate forCN=*.herokuapp.com. Closes registry#34, Build(deps): Bump dompurify from 3.4.7 to 3.4.12 #36 and Build(deps): Bump sharp, @astrojs/starlight, astro, starlight-blog and starlight-llms-txt #37.Compiler and site
John→ die-bibel.de's USFMJHN) instead of forcing a second citation system into existence.toStringskips the entry instead of expandingObject.prototype's method into a published URL.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 directalternateOf/isReferencedByedges excluded onlywithdrawnandblocked, so adeprecatedmapping 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 ondraftthat Mapping projection uses a denylist, so draft and deprecated mappings emit unqualified direct edges #45 also proposed was declined: TextRefs' guarantees attach toCanonicalReference, and theWorkprojection 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 isdraft, so this closes the hole before the first demotion rather than fixing a visible defect./reg/browser could callloadWorks()andloadSystems()./reg/works.jsonand/reg/systems.jsonnow serve one@contextand one@graphof records, sorted by key, in every status, and/reg/advertises both throughlink 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.RegistryObjectstays unreferenced on purpose: it mirrors the exported Zod union instandard/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./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.jsonand is registered indatapackage.jsonwith a byte count and asha256:hash — the artifactdata/README.mdanddata/.gitignorehave 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 istanakhat 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.ymlgained the matching glob:aliases.jsonmatched neither existing pattern andfail_on_unmatched_filesis 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.Workcarried exactly one name, and the registry browser built its filter key frompreferred_labelandkeyalone, so a scholar who typedNE,ENorNikomachische Ethikfound nothing. The registry holds Aristotle's Nicomachean Ethics, but only under that one English string, and the same gap hitPIandLXX. The optionalalternative_labelslist is folded into the existingdata-reg-itemhaystack, so the client filter needed no change, and it renders on the work record page. It flows intoworks.jsonl,/reg/works.jsonand/id/work/{key}.jsonuntouched, because all three serialize the whole record rather than a field whitelist. Entries are unique within a work, never repeat thepreferred_label, and an empty list is rejected — enforced on the authored YAML and again in the canonicalWorkschema, 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 namealt_labelssince before the schema existed; the guide now documents the real one. No registry data changes, and no route is added. The matchingdata/AGENTS.mdfix landed as registry#21 and is in this pin./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, soPlato Republic 514areached 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.jsonnames a work's preferred citation system and dropsadditional_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, andresolveInIndex()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 instrumentingwindow.fetchconfirms 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 insrc/lib/find.ts, pure and free of DOM and network, which is what makes it testable undernode:testand keeps Zod and the compiler out of the 10.7 KB browser bundle;src/lib/citation.tsis 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 usesRecordPage, which gains an optionaljsonHrefbecause 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.deprecatedrecord renders as a tombstone (fix: link-check extraction, deprecated tombstones, orphaned component, docs label (#3, #99, #48) #101, landed — closes Decide what adeprecatedrecord page shows: ADR-0004 calls it a tombstone, the templates do not #99) —deprecatedis one of the three tombstone statuses in the model, and ADR-0004,specification.md:284, theStatusenum comment andRETIRED_STATUSESin the compiler all say so, yet both record templates testedwithdrawnandblockedonly. Adeprecatedrecord therefore rendered with no banner at all, presented exactly like anactiveone, and dropped itssuperseded_bylink even though the compiler accepts the field on that status and the.jsonsibling 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 — adeprecatedrecord is retained and still resolves, while awithdrawnorblockedrecord is additionally excluded fromWorkmapping projections — so reusing the withdrawn wording would have stated something false about a record that still resolves, and givingdeprecatedits own banner class would have split a category the model keeps whole. Latent, not live: no record in the registry isdeprecatedtoday, 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.CitationBox.astrois gone (fix: link-check extraction, deprecated tombstones, orphaned component, docs label (#3, #99, #48) #101, landed — closes Delete the orphanedCitationBox.astro#48) — no consumers since feat(site)!: minimal static templates for reference pages #30 inlined Chicago and COinS rendering againstsrc/lib/citation.ts. Identicaldist/output.4d1c708, perf(site): compile the registry once per build, not twice #139, closes Every build compiles the registry twice #131) — Astro loadsastro.config.mjsin a module graph of its own, separate from the one the pages render in, sosrc/lib/registry.tswas 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 everydevstart 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 onglobalThisunderSymbol.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 undernode: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, andsrc/lib/registry.test.tsfails when the two lines are swapped, its runtime jumping from 0.3ms to 871ms as the real compile happens.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 aboutnoindex, sosrc/lib/noindex.tsbecomessrc/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 reachactive. 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 norobotsmeta, so advertising it offers a crawler mislabelled duplicate content; the set is read fromsrc/content/docs/de/and grows on its own as pages are translated, taking/de/entries from 35 to 5 — exactly the Association pages thatAGENTS.mdsays are bilingual. Therobotsmeta tag still goes on a draft alias page, which ADR-0003 requires independently of the sitemap. Seven new tests insrc/lib/sitemap.test.ts./find/(d9239bd, fix(site): let a whole-query label beat a split match in /find/ #138) —interpretranked the whole query only when the split found nothing, so any split hit permanently hid a better whole-query match. Nietzsche's registered labels includeMA,MA I,MA II/1andMA II/2; splittingMA II/1leavesMA, the label of a different work, so the query landed on that work and reportedII/1as 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.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 publishedlicenseto be the canonical SPDX IRI sodcterms:licensehas a single IRI-typed range, and the chip — written whenlicenseheld a bare identifier — put that IRI in a tag the width of a word. It now readsCC-BY-SA-3.0and links to the SPDX page.SPDX_LICENSE_BASEis exported fromscripts/compile.ts, because the compiler mints the IRI andspdxLicenseLabelreverses 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 splitlicense(dcterms:license) fromlicense_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) andsefaria.org/terms(23,213), both withaccess: open. So the NA28 and BHS rows showed oneopenchip and nothing else, which a reader can fairly take as "freely usable" when the registry recorded the opposite.license_urlnow renders as aRightschip 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 isRightsbecause it is the one word true of all sixlicense_urlvalues in the registry without inspecting any of them and it matches the predicate — anImpressumis 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 anaria-labelnaming its provider, because a page holds several targets and a screen reader would otherwise announce several identicalRightslinks. The fixture registry gained one licensed target and one rights-only target: it had neither, so no fixture-backed build rendered either chip andnpm run verify:fastcould 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.API audit follow-ups (#111)
An audit of the live site against the ReSed API-design criteria found ten items (#111). Five
landed on
stagingon 2026-08-31, in three PRs. Two were published claims the site did notmeet, which is why they are in this release rather than after it: a tagged baseline should not
name a URL that returns 404.
5914e26, fix(api): publish the contract and correct the 404 declaration (#113, #112) #117, closes fix(api): publish the OpenAPI contract at /api/openapi.yaml #112) — the sitedescription advertised
/api/openapi.yaml, and only the rendered HTML at/api/existed.starlight-openapireads the file at build time and copies nothing. A static endpoint nowserves it, with
?rawinlining the source at build time rather than duplicating it inpublic/, socmpproves the built body byte-identical toapi/openapi.yaml. A client cangenerate code and run contract tests against the same file the docs render.
5914e26, fix(api): publish the contract and correct the 404 declaration (#113, #112) #117, closes fix(api): declare the 404 response as text/html, not JSON #113) — everyrecord path declared
404asapplication/jsonagainst theErrorschema, and a static hostcannot build a per-path body. One edit to the shared
NotFoundresponse covers all 12 paths.Errorstays, marked reserved for the dynamic resolver the contract already announces, andRegistryObjectgained the same kind of comment, so no schema is unreachable without a statedreason.
0607ec6, feat(standard): publish the generated JSON Schema at /schemas/v1/ (#115) #118, closes feat(standard): publish the generated JSON Schema at /schemas/v1/ #115) —specification.md§14 stated that a normative JSON Schema is published at/schemas/v1/textrefs.schema.jsonwhileROADMAP.mdcalled the same pathplanned. Thedocument is now generated from the canonical Zod schemas with
z.toJSONSchema()on everybuild; the specification text is unchanged, and the roadmap item moved to done in both halves
of the mirrored pair. Three corrections to the raw Zod output are documented in the generator:
additionalProperties: falseis dropped, because a served body carries a JSON-LD@contextand §15 allows extensions;
format: "iri"replaces"uri", because RFC 3986 rejects thenon-ASCII resolver targets that 1219 references carry; and the §6
alternative_labelsrulestravel into the document as
uniqueItems, with the half that compares two members stated inthe field description. All 86477 records of the current pin validate against it.
/dump/has an index (cf3a416, feat(site): add the /dump/ index and robots.txt (#116, #114) #119, closes feat(site): add a /dump/ index page #116) —datapackage.jsonis theentry point for bulk access and a reader could not guess it. The page listed all six artifacts
with the size and
sha256hash of each.astro buildruns beforescripts/compile.ts, so thepage cannot read the descriptor from
dist/; it called the same pure functions the compilercalls, over the same registry, so the table and
datapackage.jsoncould not drift. fix: address PR #4 review findings before v0.1.0 (#120) #121narrowed this: computing the figures cost ~90 MB of serialization and hashing per build, to
render numbers the descriptor already publishes as machine-readable data. The page now lists the
files and links to the descriptor for the sizes and the hashes. See the review section below.
robots.txtexists and names the bulk route (cf3a416, feat(site): add the /dump/ index and robots.txt (#116, #114) #119, closes feat(site): add robots.txt and state the bulk-access route #114) —the site gave crawlers no guidance. It names the sitemap and tells a bulk consumer to read
/dump/instead of crawling thousands of record pages, and theBulktag in the contract nowstates the GitHub Pages soft limits. Nothing is disallowed: draft record pages already carry
noindexand stay out of the sitemap. It is an endpoint rather thanpublic/robots.txt, sothe
Sitemap:line followsSITE_DOMAINand a preview deployment cannot advertise theproduction sitemap.
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 thecontract), G6 (no stated HTTP versioning policy), G7 (the no-query-parameter choice is unstated),
G9 (the German
hreflangalternate on record pages 404s), and G10 (the production 404 pagecarries 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.yamlmust return 200 withtext/yaml— the value the contractrecords, confirmed against two live GitHub Pages deployments rather than assumed — and
curl -sI https://textrefs.org/id/work/does-not-exist.jsonmust return 404 withtext/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
stagingas56cab1b, which closes #120. Every finding was reproducedbefore it was fixed.
/find/matched a fragment of a name — tier 6 compared the query against one joined string,so
includesaccepted any fragment of any name:antsits insideDante, andinterpretcarried 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 passage —interprettreated every tier alike, so asingle fuzzy hit returned
resolvable. The page tells the reader "Nothing is guessed", so thatclaim was false. A fuzzy leading tier now returns
work-matches, which already meant "neverresolved further", and the page renders it as "Did you mean …?".
Republikstill finds theRepublic; it asks first.
Irideclaredformat: uri, and RFC 3986forbids the non-ASCII characters that registered resolver targets carry.
iriis a definedformat in the JSON Schema 2020-12 vocabulary that OpenAPI 3.1 uses, and
standard/schema/json-schema.tsalready applied this exact correction to the generated schema;the OpenAPI document was the last place still saying
uri. Newapi/openapi.test.tsvalidatesreal non-ASCII targets against the parsed contract, because nothing else in the build checked
data against it.
draftrecords — every record in this release isdraft, whichcarries 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.mdandthe
/reg/front page now reserve "permanent" for anactiverecord. The normative pagesalready did.
/dump/recomputed the whole dump on every build — see the corrected bullet above. The newDUMP_MANIFESTcarries the file list with no body, and a test locks it to whatdumpResourcesproduces, so the page and the descriptor still cannot disagree about which files exist. The
verify snippet on the same page printed no hash:
grep -A2stops five lines short of the fieldin the descriptor order. It uses
jqnow.record.cssinvented a palette andused
system-ui, againstAGENTS.md. Colours already flowed through custom properties, so onlythe token block changed. The file is served unbundled from
public/and cannot importbrand.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
Workexample was invalid againstWork.safeParsefor want ofpreferred_citation_system_key, the OpenAPI document described the advertised<link rel="alternate">type asapplication/jsonwhere the HTML emitsapplication/ld+jsonand omittedWork.creators, and §14 required BCP 47 validation that all three schema layers implemented asminLength: 2. That last one is now a sharedLanguageTag(RFC 5646 langtag + privateuse), mirrored as an OpenAPIpattern.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 everyContent-Typefrom the file extension and discards the header the AstroAPIRoutesets. On the live site/id/work/plato.republic.jsonreturnsapplication/json; charset=utf-8and/dump/works.jsonlreturnsapplication/octet-stream. Declaringapplication/ld+jsonwould 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 onstaging. 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 adeprecatedrecord is retained and still resolves "while awithdrawnorblockedrecord is additionally excluded fromWorkmapping projections". The compiler excludes all three:RETIRED_STATUSESis{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 forwithdrawnandblockedis 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.tshas carrieddeprecated mappings are excluded from the projectionthroughout, 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.mdandexpenses.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.mdwas untracked, and Claude Code readsCLAUDE.mdand ignoresAGENTS.md, so a fresh clone loaded no agent instructions. It is tracked now, holding the single line@AGENTS.md.AGENTS.mdalso gained the five GitHub workflow rules that governed this release but lived only inCONTRIBUTING.mdand in habit — sign every commit, branch fromstagingfor content, branch frommainfor infrastructure, open every PR from themaehrfork, squash-merge only — plus the warning that the Update-branch button and--allow-update-branchboth produce an unsigned commit, which themainruleset'srequired_signaturesthen rejects.npm run check:linksis documented;package.jsonhad 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.819fcb9, docs(association): finalise the founding documents and limit Board members to expense reimbursement (#123) #124, closes docs(association): align Art. 15 with tax authority feedback #123) — the association was founded on 2 September 2026. Statutes Art. 22 carries the adoption date and the place-and-date line, and names both signatories: Moritz Mähr as president, Luz Christopher Seiberth as recording officer. ZGB Art. 60 requires the statutes in written form, and the cantonal tax office asked for the signed statutes and the signed founding minutes. TheArbeitsentwurfaside is gone from the four German documents, because they are no longer working versions under legal reservation, and "in Gründung befindlich" / "in formation" is gone from the association index, the mission andREADME.md.rechtskräftige Verfügung, so both association index pages state that donations become tax-deductible only after the legally binding decision. Nothing published here claims more than that.bdd1d60, docs(blog): announce the founding of the association #142) — a blog post at/blog/2026/09/association-founded/, in the same terms. English only:AGENTS.mdlimits the German mirror to legally relevant Association pages, and the binding German documents are already published.blog/2026/06/hello-world.mdstill says "an association in formation" and stays unchanged, because it is a dated post and a historical record.The German texts are legally binding; the English versions are convenience translations and now
say so on every page (F36, above).
Release plumbing
CITATION.cffcarriesversionanddate-released(chore(release): v0.1.0 metadata — CITATION.cff, changelog, checklist fixes #76, closes CITATION.cff carries no version or date-released #51), and the maintainer release checklist that omitted them is fixed.CHANGELOG.mdregenerated withgit-cliff --tag v0.1.0.versioning.mdno longer claims a Frictionlessschemathe compiler does not emit; tracked in datapackage.json omits the Frictionless schema field descriptors versioning.md requires #74.astro checkrejects 7.x.github-profilesubmodule is gone (fix: release hardening before v0.1.0 (#46, #47, #49, #50) #75, closes github-profile submodule has no consumer #50), so the tag does not freeze a dependency nothing reads.)and then stripped trailing punctuation, in that order, so a URL ending a markdown link inside a sentence or a bold run kept its)and lychee checked a truncated address —…/Republic_(Plato,…/4.0/),…/arks.org/)**. The trim now runs as a loop until the string stops changing, percent-encoded resolver templates (%7B/%7D) are dropped alongside the literal{}ones, andiso.organdloc.govjoin the excludes because both answer a bot with 403. 17 errors → 3. The three that remain areblob/mainlinks to ADR-0001, ADR-0002 and ADR-0006, whichmaindoes not carry yet — merging this PR resolves them, since it brings all seven ADRs tomain. Re-run the Links workflow after the tag to confirm zero.Spec version vs tag
The tag is
v0.1.0; the specification stays at0.1.0-draftwithmaturity: working-draft. These are deliberately different things — ADR-0004 and ADR-0006 both declined a spec bump under the working-draft clause, and0.xreleases stayworking-draftby the maturity ladder. All registry data isdraftand carries no persistence promise until expert review promotes it.Verification
dataandlinkcheckre-run automatically asstagingmoves; both were green on0df2b27and on every tip since.github-pagesmust be dispatched onstagingfor the final SHA before merge. The ruleset requires a deployment for the SHA being merged, and Pages only auto-runs on push tomain— so this is repeated every timestagingmoves. Dispatched onstagingat56cab1b(fix: address PR #4 review findings before v0.1.0 (#120) #121) on 2026-08-31, which is now eleven commits stale.stagingis atba908c7and nothing further is queued, so dispatching on that SHA is the last action before approval — and it must name the SHA that is actually merged. See #68, correction 2.npm run verifyis green on the tree merged asba908c7(2026-09-03, real registry): 0astro checkerrors, 137/137 tests, 259,813 pages, all internal links valid, and the compiler reports13 system(s), 23 work(s), 86397 reference(s), 44 mapping(s); 172838 alias(es); 86477 records in dump. That branch was cut from41f2d31and does not carry docs(blog): announce the founding of the association #142, which adds the announcement post, so the tip figure is one or two pages higher. Re-run the full gate onba908c7itself before the tag if an exact page count matters for the release notes. Earlier: on the branch merged ascf3a416(2026-08-31), the fullnpm run buildreports 259,812 pages in 1m 43s and the same compiler line as below. That tree carries three of the four new routes;/schemas/v1/textrefs.schema.jsoncame afterwards with feat(standard): publish the generated JSON Schema at /schemas/v1/ (#115) #118, and no full build has run onstagingat0607ec6yet —npm run verify:fastis green there. The figures in the rest of this bullet are the earlier run ona5764fb(2026-08-27), before any of the four routes: the fullnpm run verifyis green — 0 errors, 0 warnings fromastro check, 99/99 tests, 259,811 pages built in 3m 2s, and the compiler reports13 system(s), 23 work(s), 86397 reference(s), 44 mapping(s); 172838 alias(es); 86477 records in dump. All six dump artifacts emit and matchrelease.yml's globs —works.jsonl,citation-systems.jsonl,references.jsonl,mappings.jsonl,aliases.json,datapackage.jsonat version0.1.0.release.ymlhas still never run, and the payload has grown.references.jsonlis now 69 MB rather than the 54 MB this description carried before chore(data): bump the registry pointer to 455bb27f #95 and chore(data): bump the registry pointer to 7d109195 #96, and the built site is 259,811 pages rather than 204,361. Do theworkflow_dispatchdry run the workflow already supports before tagging, and check the timeout headroom against the current build, not the old one. A failure here happens after the tag is already public.npm testis 137/137 onstagingatba908c7, measured locally on 2026-09-03 against the real registry. The nineteen cases added since56cab1bcover the ontology (feat(standard): publish the TextRefs ontology #127), the shared-predicate guard instandard/schema/context.test.ts(fix(standard): publish license_url as dcterms:rights, not dcterms:license #137), the sitemap's three rules (fix(site): keep redirects and untranslated fallbacks out of the sitemap #136), the/find/whole-query regression and its over-correction guard (fix(site): let a whole-query label beat a split match in /find/ #138), the fixture short-circuit above the registry cache (perf(site): compile the registry once per build, not twice #139), andspdxLicenseLabel(fix(site): render the resolver-target licence and rights correctly #143) — the canonical IRI, a value without the prefix, the bare prefix, and a look-alike prefix on another host. The announcement (docs(blog): announce the founding of the association #142) adds no test, and theRightschip carries no logic to unit-test; the fixture covers it in every build. Historically,npm testwas 118/118 at56cab1b: fix: address PR #4 review findings before v0.1.0 (#120) #121 added nine cases — the token haystack, the fuzzy tier that asks rather than resolves, the non-ASCII IRI against the parsed OpenAPI contract, and theDUMP_MANIFESTlock. It was 109/109 at0607ec6, where feat(standard): publish the generated JSON Schema at /schemas/v1/ (#115) #118 added ten cases for the published JSON Schema — the dialect and$idthe specification names, the union, the output-moderesolver_targetsrule, one record of each type, a served body with its@context, a non-ASCII IRI target, theuniqueItemsrule, the documented gap that no keyword expresses, and an unknowntype. It was 99/99 after feat(find): resolve a familiar citation to a canonical reference (#90) #93 (unchanged by fix: link-check extraction, deprecated tombstones, orphaned component, docs label (#3, #99, #48) #101, which touches templates, a workflow and a config, none of them under test), which added thirty-four cases for the finder — every case the spec named, plus one per ambiguity rule; feat(standard): name works by abbreviation and translated title (#85) #89 before it added eleven for alternative labels and feat(api): publish an alias index so clients resolve a locator without UUIDv5 (#84) #88 nineteen for the locator index and the dump resources, and earlier updates to this description cite 65, 54, 35, 31, 30 and 25. None of docs: resolve the consistency audit before v0.1.0 (#79) #80, fix(compile): stop projecting retired mapping assertions onto Work (#45) #86, feat(api): add works and systems collection endpoints (#83) #87, feat(api): publish an alias index so clients resolve a locator without UUIDv5 (#84) #88, feat(standard): name works by abbreviation and translated title (#85) #89 or feat(find): resolve a familiar citation to a canonical reference (#90) #93 touched registry data; the counts moved afterwards, when chore(data): bump the registry pointer to 455bb27f #95 and chore(data): bump the registry pointer to 7d109195 #96 advanced thedata/pin to7d10919. feat(find): resolve a familiar citation to a canonical reference (#90) #93 added the/find/and/get-started/finding-references/routes, plus the German fallback page Starlight emits for the latter.releaseevents for both repositories.