From 06fa0cb906a9d4906e03a8d67f44d767a92c692e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20M=C3=A4hr?= <14755525+maehr@users.noreply.github.com> Date: Thu, 13 Aug 2026 11:25:31 +0200 Subject: [PATCH 1/8] fix(standard): correct spec example, validation rules and references (#79) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consistency audit findings A1, A2, A4, A5, A6, A7, B9, B10, B12. - A1: §13's worked example and the §5 class diagram omitted the required `preferred_citation_system_key`, so the spec's headline example failed `Work.safeParse`. Verified: it now parses, and did not before. - A2: clarify that the three tombstone statuses differ in reach — `deprecated` still resolves and stays projected, `withdrawn`/`blocked` do not. The definition itself is unchanged (ADR-0004). - A4: §14's requirement 10 demanded SPDX *identifier* syntax, which every conforming published record fails — published records carry the SPDX IRI. - A5: one `resolver_targets` array carried both license encodings. - A6: drop the unenforceable "presence for language-specific entries" from the validator list; §9 keeps it as an authoring obligation. - A7: `license` is a build failure, not a SHOULD, and deprecated SPDX ids are accepted, so "current" overstated the rule. - B9/B10: SKOS no longer carries mapping relations after ADR-0006; add the missing PROV-O row. - B12: `MappingAssertion` is a curated relation, not an equivalence — `isReferencedBy` targets a document *about* the work. Swept the §4 and §11 framing too, so the type is described consistently. Refs #79 Co-Authored-By: Claude Opus 5 --- src/content/docs/standard/specification.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/content/docs/standard/specification.md b/src/content/docs/standard/specification.md index f1f3285..f5374fe 100644 --- a/src/content/docs/standard/specification.md +++ b/src/content/docs/standard/specification.md @@ -28,7 +28,7 @@ A dataset conforms to the TextRefs Standard if it satisfies all of the following 4. Every `CanonicalReference` points to one known `Work` and one known `CitationSystem`. 5. Every `CanonicalReference.locator` validates syntactically against the referenced `CitationSystem` and semantically by being a registered reference point for the referenced `Work`. 6. Every `CitationSystem` declares a `description` of its citation tradition and a `locator_regex` that is a valid ECMAScript regular expression. -7. Every dereferenceable location is represented as an entry in the `resolver_targets` array of its `CanonicalReference`, and every external identifier or cross-reference equivalence through a `MappingAssertion`. +7. Every dereferenceable location is represented as an entry in the `resolver_targets` array of its `CanonicalReference`, and every external identifier or cross-reference through a `MappingAssertion`. 8. Every registry object includes administrative metadata. 9. Registry records contain identifiers, metadata, mappings, provenance, and resolver targets rather than primary text content. @@ -41,7 +41,7 @@ The key words `MUST`, `MUST NOT`, `REQUIRED`, `SHALL`, `SHALL NOT`, `SHOULD`, `S TextRefs separates **identity** from **location**. - **Identity** is abstract and language-independent. `Work`, `CitationSystem`, and `CanonicalReference` answer the question "_which_ passage": for example _the New Testament, book-chapter-verse, John.3.16_. There is exactly one such identity, regardless of how many editions, translations, or websites carry it. -- **Location and equivalence** answer "_where_ can I read it" and "_what else_ is this the same as". The `resolver_targets` array embedded in each `CanonicalReference` lists places where the reference can be read (specific translations, editions, or providers). `MappingAssertion` records that a `Work` is equivalent to an external identifier or to another `Work`. +- **Location and mapping** answer "_where_ can I read it" and "_what else_ relates to it". The `resolver_targets` array embedded in each `CanonicalReference` lists places where the reference can be read (specific translations, editions, or providers). `MappingAssertion` relates a `Work` to an external identifier or to another `Work` — either as another entity denoting the same work, or as a document about it ([§10](#10-mappingassertion)). A reference such as `John.3.16` is the **same identity** whether read in Greek, the King James Version, or the Lutherbibel. The translation is a property of the _location_, never of the identity. This is what lets the model scale to works with many editions and translations (see [§13](#13-worked-example-a-multi-translation-work)). @@ -56,7 +56,7 @@ A conforming registry MUST support these object types. Each top-level object MUS | `Work` | identity | An abstract textual work. | | `CitationSystem` | identity | A notation that fragments works into locators. | | `CanonicalReference` | identity + location | One abstract reference point in a work, with embedded resolver targets. | -| `MappingAssertion` | equivalence | A curated equivalence between a `Work` and an external identifier. | +| `MappingAssertion` | relation | A curated relation between a `Work` and an external identifier. | Dereferenceable locations are not a separate object type. They are recorded as entries in the `resolver_targets` array embedded in each `CanonicalReference` (see [§9](#9-embedded-resolver-targets)). This keeps language-tagged locations co-located with the reference they describe, and means a work with N translations adds N array entries — not N standalone records. @@ -68,6 +68,7 @@ classDiagram +URI id +string key +string preferred_label + +string preferred_citation_system_key +Creator[] creators } class CitationSystem { @@ -230,14 +231,14 @@ Required per entry: `url`, `access`. - `url` MUST be a dereferenceable external IRI ([RFC 3987](https://www.rfc-editor.org/rfc/rfc3987)). - `language` MUST be present when the entry is language-specific (e.g. a translation), as a [BCP 47](https://www.rfc-editor.org/info/bcp47) language tag ([RFC 5646](https://www.rfc-editor.org/rfc/rfc5646)). Tags MUST include an [ISO 15924](https://www.unicode.org/iso15924/) script subtag when the entry uses a non-default script for the language (e.g. `grc-Grek`, `hbo-Hebr`, `grc-Latn`). `edition` SHOULD name the specific edition or version when known. - `access` MUST be one of `open`, `paywalled`, `restricted`, `unknown`. -- `license` SHOULD be authored as a current [SPDX license identifier](https://spdx.org/licenses/) (e.g. `CC0-1.0`, `CC-BY-4.0`) when the licence of the target resource is known; published JSON-LD carries the canonical SPDX IRI (`https://spdx.org/licenses/{id}`), so `dcterms:license` has a single IRI-typed range. For licences not in the SPDX list, omit `license` and use the optional `license_url` to point at the licence text. +- `license`, when present, MUST be a current or deprecated [SPDX license identifier](https://spdx.org/licenses/) (e.g. `CC0-1.0`, `CC-BY-4.0`); published JSON-LD carries the canonical SPDX IRI (`https://spdx.org/licenses/{id}`), so `dcterms:license` has a single IRI-typed range. For licences not in the SPDX list, omit `license` and use the optional `license_url` to point at the licence text. - Values implying permission to host copyrighted full text (e.g. a `license` of `proprietary` accompanied by hosted text) are forbidden; the no-text rule in [§2](#2-conformance) governs. - A `CanonicalReference` whose `resolver_targets` is an empty array remains a valid identity record; adding or removing an entry MUST NOT change the parent reference's `id`. - Tombstoning a single bad URL is done by removing the entry; tombstoning the whole reference uses the parent `status` field. There is no independent status on individual entries. ## 10. MappingAssertion -A `MappingAssertion` records a curated equivalence claim between a TextRefs `Work` and an **external identifier** (CTS URN, Wikidata Q-ID, DOI, ARK, …) or another TextRefs `Work`. There is no separate object type for external identifiers; they are always expressed as mapping targets. +A `MappingAssertion` records a curated relation between a TextRefs `Work` and an **external identifier** (CTS URN, Wikidata Q-ID, DOI, ARK, …) or another TextRefs `Work`. There is no separate object type for external identifiers; they are always expressed as mapping targets. ```json { @@ -276,7 +277,7 @@ A `MappingAssertion` identifier MUST be generated deterministically from `subjec The persistence promise attaches at **promotion**: the first time a record is published at status `active` ([§12](#12-administrative-metadata)). Promotion changes `status` only and MUST NOT change identity-defining fields, so the identifier survives promotion unchanged. Records at status `draft` are excluded from the persistence policy: they MAY be corrected (changing an identity field mints a different identifier; the previous one ceases to resolve) or retracted (the record is deleted) without a tombstone. -An implementation MUST NOT silently change the identity-defining fields of an existing **promoted** `CanonicalReference`. Because those fields seed the deterministic identifier, any change produces a new `CanonicalReference` with a new identifier. The prior reference MUST be retained as a tombstone (`status` `deprecated`, `withdrawn`, or `blocked`, [§12](#12-administrative-metadata)) and SHOULD carry the successor IRI in `superseded_by`. `MappingAssertion`s MUST NOT be used for succession; they are reserved for work-level equivalence ([§10](#10-mappingassertion)). +An implementation MUST NOT silently change the identity-defining fields of an existing **promoted** `CanonicalReference`. Because those fields seed the deterministic identifier, any change produces a new `CanonicalReference` with a new identifier. The prior reference MUST be retained as a tombstone (`status` `deprecated`, `withdrawn`, or `blocked`, [§12](#12-administrative-metadata)) and SHOULD carry the successor IRI in `superseded_by`. `MappingAssertion`s MUST NOT be used for succession; they are reserved for work-level relations to external identifiers ([§10](#10-mappingassertion)). The three tombstone statuses differ in reach: a `deprecated` record is retained and still resolves, while a `withdrawn` or `blocked` record is additionally excluded from `Work` mapping projections ([§6](#6-work)) and MUST NOT be depended on by a live record. A conforming registry SHOULD publish each `/id/{type}/{key}` IRI at two static URLs: the canonical URL itself (HTML for browsers) and a sibling with a `.json` extension carrying the JSON-LD payload. The HTML representation SHOULD advertise the JSON-LD sibling via `` in the document head. `Accept`-header content negotiation is not required. @@ -321,6 +322,7 @@ This is the case that motivates separating identity from location. The New Testa "key": "new-testament", "type": "Work", "preferred_label": "New Testament", + "preferred_citation_system_key": "bible-book-chapter-verse", "status": "active", "created": "2026-05-31", "modified": "2026-05-31" @@ -357,7 +359,7 @@ This is the case that motivates separating identity from location. The New Testa "edition": "King James Version", "provider": "Bible Gateway", "access": "open", - "license": "CC0-1.0" + "license": "https://spdx.org/licenses/CC0-1.0" } ], "status": "active", @@ -385,7 +387,7 @@ A conforming validator MUST check: 7. canonical-reference semantic validity: accepted records must be registered, attested reference points for their `Work` and `CitationSystem`; 8. deterministic-identifier correctness for canonical references and mapping assertions; 9. UUID-based identifier shape for `CanonicalReference` and `MappingAssertion` records; -10. `resolver_targets` entries: `access` values, BCP 47 syntax of `language` and its presence for language-specific entries, and SPDX syntax of `license` when present; +10. `resolver_targets` entries: `access` values, BCP 47 syntax of `language`, and SPDX license IRI syntax of `license` when present ([§9](#9-embedded-resolver-targets)) — the bare SPDX identifier form is an authoring-time check, not a published-record validation concern; 11. mapping `relation` values and the Work-IRI shape of `MappingAssertion.subject`; 12. absence of forbidden full-text/apparatus/commentary content; 13. that no `active` record depends on a `draft` one: an `active` `CanonicalReference` references an `active` `Work` and an `active` `CitationSystem`, and an `active` `MappingAssertion` takes an `active` `Work` as its `subject`; `Work.preferred_citation_system_key` MUST reference a known `CitationSystem`, and an `active` `Work` requires an `active` preferred `CitationSystem`. @@ -415,7 +417,8 @@ This standard relies on the following external standards. Each is normative wher | Regular expression dialect | [ECMA-262](https://262.ecma-international.org/) §22.2 | | Versioning | [SemVer 2.0.0](https://semver.org/spec/v2.0.0.html) | | Linked-data serialization | [JSON-LD 1.1](https://www.w3.org/TR/json-ld11/) | -| Concepts and mapping relations | [SKOS](https://www.w3.org/TR/skos-reference/) | +| Labels and concept schemes | [SKOS](https://www.w3.org/TR/skos-reference/) | +| Alternate-presentation relations | [PROV-O](https://www.w3.org/TR/prov-o/) | | Dates, provenance, language, licence | [Dublin Core Terms](https://www.dublincore.org/specifications/dublin-core/dcmi-terms/) | | URL, provider, edition, work type | [schema.org](https://schema.org/) | | Licence identifiers | [SPDX License List](https://spdx.org/licenses/) | From 625ed4a72c1150aa7a1799c4965be0043ed4cc93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20M=C3=A4hr?= <14755525+maehr@users.noreply.github.com> Date: Thu, 13 Aug 2026 11:25:46 +0200 Subject: [PATCH 2/8] fix(api): align OpenAPI with the contract and validate BCP 47 tags (#79) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consistency audit findings A3, D22, A6, plus the A2 wording spillover. - A3: the advertised alternate-link media type was `application/json`; the implementation emits `application/ld+json` and the spec requires it, so OpenAPI was the incorrect side. - D22: `Work.creators` is documented, implemented and in the published context, but was missing from the OpenAPI schema. Adds a `Creator` component mirroring the Zod discriminated union. - A6: §14 required validators to check BCP 47 syntax of `language`, but nothing did — all three layers had only `minLength: 2`. Adds a shared `LanguageTag` (RFC 5646 langtag + privateuse well-formedness) to `standard/schema/common.ts`, wires it into the published and source schemas, and mirrors it as an OpenAPI `pattern`. - A2: "non-tombstoned" was ambiguous — the projection filter is narrower than the spec's tombstone definition. Names the actual statuses instead. All 12 language values in the registry still validate; `compile:data` compiles 67,959 references with no rejections. Refs #79 Co-Authored-By: Claude Opus 5 --- api/openapi.yaml | 38 +++++++++++++++++++++----- scripts/compile.test.ts | 23 ++++++++++++++++ scripts/source-schema.ts | 9 ++++-- standard/schema/canonical-reference.ts | 4 +-- standard/schema/common.ts | 10 +++++++ standard/schema/work.ts | 4 +-- 6 files changed, 75 insertions(+), 13 deletions(-) diff --git a/api/openapi.yaml b/api/openapi.yaml index 76bd354..816b02a 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -10,7 +10,7 @@ info: - `/id/{type}/{key}.json` — JSON-LD for machines. There is **no** `Accept`-header content negotiation. Clients either - follow the `` + follow the `` advertised in the HTML head, or simply append `.json` to the canonical URL. JSON bodies carry the JSON-LD `@context` at [`/contexts/v1.jsonld`](https://textrefs.org/contexts/v1.jsonld); the @@ -196,6 +196,24 @@ components: description: Successor IRI. Only present when status is deprecated, withdrawn, or blocked. + Creator: + description: + Discriminated on `kind`. A `person` entry names a family name (and + optional given name); mononyms such as Plato use `family` alone. A + `literal` entry is the escape hatch for pseudonymous, collective, or + institutional authorship. + oneOf: + - type: object + required: [kind, family] + properties: + kind: { type: string, enum: [person] } + family: { type: string, minLength: 1 } + given: { type: string, minLength: 1 } + - type: object + required: [kind, name] + properties: + kind: { type: string, enum: [literal] } + name: { type: string, minLength: 1 } Work: allOf: - $ref: '#/components/schemas/AdminMetadata' @@ -217,18 +235,21 @@ components: Governs the bare /cite/{work_key}/{locator} alias and default presentation only; it is identity-neutral and never affects how a fully qualified reference validates or resolves. + creators: + type: array + items: { $ref: '#/components/schemas/Creator' } alternateOf: type: array items: { $ref: '#/components/schemas/Iri' } description: - Compiler-derived from the work's non-tombstoned alternateOf - MappingAssertions. Read-only, never authored. + Compiler-derived from the work's alternateOf MappingAssertions + that are not withdrawn or blocked. Read-only, never authored. isReferencedBy: type: array items: { $ref: '#/components/schemas/Iri' } - description: - Compiler-derived from the work's non-tombstoned isReferencedBy - MappingAssertions. Read-only, never authored. + description: Compiler-derived from the work's isReferencedBy + MappingAssertions that are not withdrawn or blocked. + Read-only, never authored. CitationSystem: allOf: - $ref: '#/components/schemas/AdminMetadata' @@ -248,7 +269,10 @@ components: required: [url, access] properties: url: { $ref: '#/components/schemas/Iri' } - language: { type: string, minLength: 2 } + language: + type: string + minLength: 2 + pattern: '^(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}){0,3}|[A-Za-z]{4,8})(?:-[A-Za-z]{4})?(?:-(?:[A-Za-z]{2}|\d{3}))?(?:-(?:[\dA-Za-z]{5,8}|\d[\dA-Za-z]{3}))*(?:-[\dA-WY-Za-wy-z](?:-[\dA-Za-z]{2,8})+)*(?:-[Xx](?:-[\dA-Za-z]{1,8})+)?$|^[Xx](?:-[\dA-Za-z]{1,8})+$' edition: { type: string } provider: { type: string } access: diff --git a/scripts/compile.test.ts b/scripts/compile.test.ts index b6a70f8..6c09d5e 100644 --- a/scripts/compile.test.ts +++ b/scripts/compile.test.ts @@ -568,3 +568,26 @@ test('url_by with more than one selector variable is rejected at parse time', (t ); assert.match(logged.join('\n'), /url_by takes exactly one selector variable/); }); + +// --- Resolver language tags (BCP 47) ----------------------------------------- + +test('a malformed language tag is rejected at parse time', (t) => { + const message = expectCompileError( + t, + workWithResolver(` - url: 'https://example.org/{book}/{chapter}' + language: 'e n'`), + ); + assert.match(message, /test\.work/); +}); + +test('a well-formed subtagged language tag is accepted', () => { + const reg = compileFixture( + workWithResolver(` - url: 'https://example.org/{book}/{chapter}' + language: grc-Grek`), + ); + assert.equal( + reg.references.find((r) => r.locator === 'Gen.1')?.resolver_targets[0] + ?.language, + 'grc-Grek', + ); +}); diff --git a/scripts/source-schema.ts b/scripts/source-schema.ts index c96ea32..717abd3 100644 --- a/scripts/source-schema.ts +++ b/scripts/source-schema.ts @@ -6,7 +6,12 @@ // at all. Objects are strict: an unknown key is an authoring error. import { createRequire } from 'node:module'; import { z } from 'zod'; -import { FlatKey, IsoDate, Status } from '../standard/schema/common.js'; +import { + FlatKey, + IsoDate, + LanguageTag, + Status, +} from '../standard/schema/common.js'; const require = createRequire(import.meta.url); const spdxLicenseIds: string[] = require('spdx-license-ids'); @@ -39,7 +44,7 @@ export const ResolverEntrySource = z .optional(), provider: z.string().min(1).optional(), edition: z.string().min(1).optional(), - language: z.string().min(2).optional(), + language: LanguageTag.optional(), access: z.enum(['open', 'paywalled', 'restricted', 'unknown']).optional(), // Checked here rather than at emit time: the compiler maps an id to its // canonical SPDX IRI, and anything it cannot map would otherwise drop out diff --git a/standard/schema/canonical-reference.ts b/standard/schema/canonical-reference.ts index 9bc8f54..f3356a7 100644 --- a/standard/schema/canonical-reference.ts +++ b/standard/schema/canonical-reference.ts @@ -1,9 +1,9 @@ import { z } from 'zod'; -import { AdminMetadata, FlatKey, Iri, IsoDate } from './common.js'; +import { AdminMetadata, FlatKey, Iri, IsoDate, LanguageTag } from './common.js'; export const ResolverTargetEntry = z.object({ url: Iri, - language: z.string().min(2).optional(), + language: LanguageTag.optional(), edition: z.string().optional(), provider: z.string().optional(), access: z.enum(['open', 'paywalled', 'restricted', 'unknown']), diff --git a/standard/schema/common.ts b/standard/schema/common.ts index 88114f1..a92c085 100644 --- a/standard/schema/common.ts +++ b/standard/schema/common.ts @@ -30,3 +30,13 @@ export const AdminMetadata = z.object({ export const FlatKey = z .string() .regex(/^[a-z0-9][a-z0-9._-]*$/, 'flat key syntax: ^[a-z0-9][a-z0-9._-]*$'); + +// BCP 47 well-formedness (RFC 5646 langtag + privateuse). Registry validity is +// not checked — only that the tag parses. Irregular grandfathered tags +// (i-klingon, …) are out of scope: no text language in the registry needs one. +export const LanguageTag = z + .string() + .regex( + /^(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}){0,3}|[A-Za-z]{4,8})(?:-[A-Za-z]{4})?(?:-(?:[A-Za-z]{2}|\d{3}))?(?:-(?:[\dA-Za-z]{5,8}|\d[\dA-Za-z]{3}))*(?:-[\dA-WY-Za-wy-z](?:-[\dA-Za-z]{2,8})+)*(?:-[Xx](?:-[\dA-Za-z]{1,8})+)?$|^[Xx](?:-[\dA-Za-z]{1,8})+$/, + 'must be a well-formed BCP 47 language tag', + ); diff --git a/standard/schema/work.ts b/standard/schema/work.ts index 3517ad5..656fa57 100644 --- a/standard/schema/work.ts +++ b/standard/schema/work.ts @@ -27,8 +27,8 @@ export const WorkBase = AdminMetadata.extend({ // known CitationSystem. preferred_citation_system_key: FlatKey, creators: z.array(Creator).optional(), - // Compiler-derived projection of the work's non-tombstoned - // MappingAssertions (prov:alternateOf / dcterms:isReferencedBy in the + // Compiler-derived projection of the work's MappingAssertions that are not + // withdrawn or blocked (prov:alternateOf / dcterms:isReferencedBy in the // published context, ADR-0006). Never authored. alternateOf: z.array(Iri).optional(), isReferencedBy: z.array(Iri).optional(), From aba37b1ae9982719aff074e593e6e804cdd84ab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20M=C3=A4hr?= <14755525+maehr@users.noreply.github.com> Date: Thu, 13 Aug 2026 11:26:01 +0200 Subject: [PATCH 3/8] docs(community): resync mirrored contributing and roadmap (#79) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consistency audit findings F32, F33, F34, F35, G37, G38, G39, D26. - F32: the published contributing page told contributors to branch from and PR against `main`, and omitted the entire `## Branching model` section. The actual infrastructure integrates on `staging` — Dependabot targets it and the registry's validate workflow checks out `parent/staging`. Most user-facing item in the audit. - F33: the site's release checklist had drifted to 7 steps and prescribed bare `npm run changelog`, which the canonical file warns files commits under `## [Unreleased]`. It also dropped the CITATION.cff and Pages-dispatch steps — the latter explains why a release PR stays blocked. Ported the canonical 9 steps. - F34: `AGENTS.md` did not list `ROADMAP.md` as a mirror pair, though ROADMAP.md declares itself one — plausibly how F32/F33 arose. - F35: the mirrored roadmap page linked to itself. - G37: README documented a `docs-internal/` directory that does not exist, and omitted `src/pages/`, `src/layouts/`, `src/lib/`. Also corrects the stale "scaffolds reserved for future repo splits" note in both README and AGENTS.md — those directories now hold the live schemas and the OpenAPI contract. - G38: the conformance-failure template required test IDs from a suite the roadmap lists as planned, so the field could not be satisfied. - G39: the "replace community placeholder pages" item was stale; all seven pages carry substantive prose and none says "placeholder". - D26: `verify` and `verify:fast` both run `npm test`, which four descriptions omitted. Refs #79 Co-Authored-By: Claude Opus 5 --- .../ISSUE_TEMPLATE/conformance_failure.yml | 4 +-- AGENTS.md | 9 ++--- CONTRIBUTING.md | 2 +- README.md | 9 +++-- ROADMAP.md | 2 +- src/content/docs/community/contributing.md | 34 +++++++++++++------ src/content/docs/community/roadmap.md | 4 +-- 7 files changed, 41 insertions(+), 23 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/conformance_failure.yml b/.github/ISSUE_TEMPLATE/conformance_failure.yml index f1a74ff..a97c8e8 100644 --- a/.github/ISSUE_TEMPLATE/conformance_failure.yml +++ b/.github/ISSUE_TEMPLATE/conformance_failure.yml @@ -23,9 +23,9 @@ body: id: failing-cases attributes: label: Failing test cases - description: List the conformance test IDs that fail and what your implementation produces instead. + description: Describe the failing case and what your implementation produces instead. Include conformance test IDs if the conformance suite is available. validations: - required: true + required: false - type: textarea id: analysis attributes: diff --git a/AGENTS.md b/AGENTS.md index b1131c6..07fea0b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,8 +9,8 @@ Astro + Starlight site for textrefs.org. See [`README.md`](./README.md) for the - `npm run build:fast` — site build against a tiny fixture registry, without compiling full data - `npm run format` — Prettier (with `prettier-plugin-astro`) - `npm run check` — `astro check` -- `npm run verify:fast` — fast local verification: format:check + fixture-backed check + fixture-backed build -- `npm run verify` — format:check + check + build (the CI gate) +- `npm run verify:fast` — fast local verification: format:check + fixture-backed check + tests + fixture-backed build +- `npm run verify` — format:check + check + tests + build (the CI gate) - `npm run validate:data` / `compile:data` / `build:data` — registry-data gates; see [`data/AGENTS.md`](./data/AGENTS.md) Use `npm run verify:fast` by default for local validation and agent work. Only use the full `npm run verify` and/or `npm run validate:data` when changes affect registry data, release artifacts, production build behavior, or CI parity. @@ -29,7 +29,8 @@ Husky installs on `npm install`: Prettier runs on staged files, `commitlint` enf - `src/styles/brand.css` — brand tokens - `public/{logo.svg,favicon.svg,fonts/}` — brand assets - `data/` — git submodule pointing at [`textrefs/registry`](https://github.com/textrefs/registry); hand-authored YAML lives there. See [`data/AGENTS.md`](./data/AGENTS.md) for authoring rules and [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the submodule workflow. -- `standard/`, `api/` — scaffolds reserved for future repo splits +- `standard/` — draft standard workspace: canonical Zod schemas (`standard/schema/`) +- `api/` — the OpenAPI contract (`api/openapi.yaml`) ## Conventions @@ -47,7 +48,7 @@ Husky installs on `npm install`: Prettier runs on staged files, `commitlint` enf ## Mirrored community files -`CODE_OF_CONDUCT.md`, `CONTRIBUTING.md`, and `SECURITY.md` live at the repo root (for GitHub's Community profile) and are mirrored at `src/content/docs/community/` with Starlight frontmatter. Edit both in the same commit. +`CODE_OF_CONDUCT.md`, `CONTRIBUTING.md`, and `SECURITY.md` live at the repo root for GitHub's Community profile; `ROADMAP.md` lives there as the canonical roadmap text. All four are mirrored at `src/content/docs/community/` with Starlight frontmatter. Edit both halves of a pair in the same commit. ## Pointers diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 30e92d9..3d5dfd1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -66,7 +66,7 @@ Registry data lives in [`textrefs/registry`](https://github.com/textrefs/registr Before pushing routine documentation, styling, or route work, run the fast local gate: ```sh -npm run verify:fast # Prettier check + fixture-backed astro check + fixture-backed build +npm run verify:fast # Prettier check + fixture-backed astro check + tests + fixture-backed build ``` Run the full `npm run verify` before PRs that touch registry data, release output, production build behaviour, or CI behaviour. Run `npm run validate:data` as well for registry-data and standard PRs. diff --git a/README.md b/README.md index 823d7c5..bce8fb7 100644 --- a/README.md +++ b/README.md @@ -49,13 +49,16 @@ TextRefs is a non-profit infrastructure project that builds, maintains, and publ ├── src/ │ ├── components/ # Starlight component overrides (Footer) │ ├── content/docs/ # site content (English at root, German under de/) +│ ├── layouts/ # shared page layouts (e.g. canonical record pages) +│ ├── lib/ # registry loading, citation, and banner helpers +│ ├── pages/ # id/, reg/, cite/ routes (see AGENTS.md) │ ├── styles/brand.css # brand tokens (see public/BRAND notes) │ └── content.config.ts ├── data/ # git submodule → textrefs/registry (hand-authored YAML) ├── scripts/ # data compile + validate pipeline -├── standard/, api/ # scaffolds reserved for future repo splits +├── standard/ # draft standard workspace: Zod schemas, JSON-LD context +├── api/ # OpenAPI contract (api/openapi.yaml) ├── decisions/ # Architecture Decision Records (MADR) -├── docs-internal/ # maintainer-only notes, not published ├── astro.config.mjs # Astro + Starlight config (i18n, sidebar) ├── cliff.toml # git-cliff config for CHANGELOG generation ├── commitlint.config.js # conventional-commit enforcement @@ -79,7 +82,7 @@ Configuration lives in `.env`; use [`.env.example`](./.env.example) as the start | `npm run validate:data` | Validate every compiled record against the canonical Zod schemas | | `npm run build:data` | `compile:data` then `validate:data` — the contributor data pipeline | | `npm run verify:fast` | Fast local check using fixture registry data | -| `npm run verify` | Prettier + `astro check` + production build — the CI gate | +| `npm run verify` | Prettier + `astro check` + tests + production build — the CI gate | | `npm run changelog` | Regenerate `CHANGELOG.md` from git history (git-cliff) | Contributors edit the YAML under [`data/works/`](https://github.com/textrefs/registry/tree/main/works) and [`data/systems/`](https://github.com/textrefs/registry/tree/main/systems); the directory is a git submodule pointing at [`textrefs/registry`](https://github.com/textrefs/registry). Run `git submodule update --init --recursive` after cloning. The compiler expands the pinned submodule into the flat registry dump (works, systems, refs, mappings) under `dist/dump/`. See [`docs/get-started/authoring`](https://textrefs.org/get-started/authoring/) for the format. For documentation, styling, and route work, use `npm run verify:fast` locally; run the full `npm run verify` before PRs that touch registry data, release output, production build behaviour, or CI behaviour. diff --git a/ROADMAP.md b/ROADMAP.md index 08fb26d..8c70240 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -18,7 +18,7 @@ Status legend: done · in progress · planned · blocked. - done: Get-started section (welcome, use cases, related identifier systems, mappings, and authoring guidance). - done: Standard draft workspace with Zod schemas and JSON-LD context. - done: Seed registry data and static registry browsing pages. -- in progress: Replace remaining community placeholder pages with contributor-facing guidance. +- done: Replace remaining community placeholder pages with contributor-facing guidance. - done: Serve canonical record pages under `/id/work/{key}/`, `/id/system/{key}/`, `/id/ref/{uuid}/`, `/id/mapping/{uuid}/`, each with a `.json` JSON-LD sibling advertised via ``. - done: Decide how the `data/` repo split is packaged — extracted to [`textrefs/registry`](https://github.com/textrefs/registry) and included here as a git submodule. diff --git a/src/content/docs/community/contributing.md b/src/content/docs/community/contributing.md index cf2ce1e..797f074 100644 --- a/src/content/docs/community/contributing.md +++ b/src/content/docs/community/contributing.md @@ -71,7 +71,7 @@ Registry data lives in [`textrefs/registry`](https://github.com/textrefs/registr Before pushing routine documentation, styling, or route work, run the fast local gate: ```sh -npm run verify:fast # Prettier check + fixture-backed astro check + fixture-backed build +npm run verify:fast # Prettier check + fixture-backed astro check + tests + fixture-backed build ``` Run the full `npm run verify` before PRs that touch registry data, release output, production build behaviour, or CI behaviour. Run `npm run validate:data` as well for registry-data and standard PRs. @@ -97,13 +97,25 @@ The commit-msg hook (commitlint) rejects non-conforming messages, so a plain `gi The changelog is generated from this history via `npm run changelog` (git-cliff). +## Branching model + +The production site (`textrefs.org`) is built and deployed from `main`. To keep `main`'s history low-noise while still allowing many small content edits, day-to-day docs/blog/copy work batches on a long-lived `staging` branch and is squash-merged into `main` to publish. + +- `main` — production source. Pushes here auto-deploy via `.github/workflows/pages.yml`. Release tags (`vX.Y.Z`) are cut from `main`; the registry's `vYYYY.MM.N` tags are cut in [`textrefs/registry`](https://github.com/textrefs/registry). +- `staging` — long-lived batching branch for docs, blog posts, copy, registry-pointer bumps, and other content edits. Does **not** auto-deploy. Edits accumulate here as many small commits. +- To publish: open a PR `staging → main` and squash-merge. The squash-commit lands on `main` as one conventional commit (so `git-cliff` stays clean) and triggers the production deploy. +- Manual preview / ad-hoc deploy: from the GitHub Actions UI, run the **Pages** workflow via `workflow_dispatch` and pick `staging` (or any branch) as the ref. This deploys that ref to production until the next push to `main`. There is no separate preview URL — GitHub Pages serves a single site per repo, so manual staging deploys temporarily replace production. Use sparingly. +- Infrastructure changes (CI, release workflow, build tooling, deploy config) target `main` directly so they are not gated on the next staging-to-main snapshot. + +Squash merging is the only enabled merge style on the canonical repo, so `staging`'s noisy history is collapsed into a single conventional-commit message on `main` and `git-cliff` still produces a clean `CHANGELOG.md`. + ## Submitting a pull request -1. Branch from `main`. +1. Branch from `staging` for content/docs/blog; branch from `main` for infra, CI, or release-workflow changes. 2. Keep PRs focused — one logical change per PR. 3. Link related issues in the PR description. 4. Include local verification results: `npm run verify:fast` for routine work, or `npm run verify` plus `npm run validate:data` for registry-data, standard, release, production-build, or CI changes. -5. Open the PR against `main`. GitHub requests `@textrefs/maintainers` by default via `.github/CODEOWNERS`; maintainers may add technical or expert reviewers based on the track. +5. Open the PR against the branch you started from (`staging` or `main`). GitHub requests `@textrefs/maintainers` by default via `.github/CODEOWNERS`; maintainers may add technical or expert reviewers based on the track. ## Project layout @@ -115,13 +127,15 @@ Two release trains. The Zenodo–GitHub webhook MUST be enabled once per reposit **Standard + site** (this repo): -1. Bump `version` in `package.json` to match the new tag. -2. `npm run changelog` to regenerate `CHANGELOG.md`. -3. Update spec page frontmatter `maturity:` if the release transitions the ladder. -4. Commit, open PR, merge to `main`. -5. Tag `vX.Y.Z[-pre]` on `main`; push the tag. -6. Verify the GitHub Release fires and Zenodo mints the version DOI. -7. Fill the concept DOI into `CITATION.cff` `identifiers:` and the badge in `README.md` (once, after the first release). +1. Bump `version` in `package.json` to match the new tag. The compiler reads it, so it also becomes the `datapackage.json` version of the published dump. +2. Set `version` and `date-released` in `CITATION.cff` to the same tag and its release date. Without them the file cannot say which release it describes. +3. `npx git-cliff --tag vX.Y.Z -o CHANGELOG.md` to regenerate `CHANGELOG.md`. Pass `--tag` explicitly: the tag does not exist yet at this point, and bare `npm run changelog` would file the commits under `## [Unreleased]`. +4. Update spec page frontmatter `maturity:` if the release transitions the ladder. +5. Dispatch the **Pages** workflow on `staging`. `main`'s ruleset requires a successful `github-pages` deployment for the exact SHA being merged, so the release PR stays blocked until the branch tip has one. +6. Open a PR `staging → main` and squash-merge it. The squash message should be a conventional commit (`docs(release): vX.Y.Z` or similar) so the changelog stays clean. +7. Tag `vX.Y.Z[-pre]` on `main`; push the tag. +8. Verify the GitHub Release fires and Zenodo mints the version DOI. +9. Fill the concept DOI into `CITATION.cff` `identifiers:` and the badge in `README.md` (once, after the first release). **Registry** ([`textrefs/registry`](https://github.com/textrefs/registry)): diff --git a/src/content/docs/community/roadmap.md b/src/content/docs/community/roadmap.md index 4374a21..a2365a1 100644 --- a/src/content/docs/community/roadmap.md +++ b/src/content/docs/community/roadmap.md @@ -5,7 +5,7 @@ sidebar: order: 7 --- -This is the public roadmap. The same content is mirrored on the docs site at [textrefs.org/community/roadmap/](/community/roadmap/). +This is the public roadmap. The canonical text is [`ROADMAP.md`](https://github.com/textrefs/textrefs.org/blob/main/ROADMAP.md) in the repo; this page mirrors it. ## Status @@ -23,7 +23,7 @@ Status legend: done · in progress · planned · blocked. - done: Get-started section (welcome, use cases, related identifier systems, mappings, and authoring guidance). - done: Standard draft workspace with Zod schemas and JSON-LD context. - done: Seed registry data and static registry browsing pages. -- in progress: Replace remaining community placeholder pages with contributor-facing guidance. +- done: Replace remaining community placeholder pages with contributor-facing guidance. - done: Serve canonical record pages under `/id/work/{key}/`, `/id/system/{key}/`, `/id/ref/{uuid}/`, `/id/mapping/{uuid}/`, each with a `.json` JSON-LD sibling advertised via ``. - done: Decide how the `data/` repo split is packaged — extracted to [`textrefs/registry`](https://github.com/textrefs/registry) and included here as a git submodule. From 06d329de5ccb135f42c6cccf90bc4d259fadcfaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20M=C3=A4hr?= <14755525+maehr@users.noreply.github.com> Date: Thu, 13 Aug 2026 11:26:12 +0200 Subject: [PATCH 4/8] docs(standard): fix json-ld, versioning and index drift (#79) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consistency audit findings A2, A8, B11, B12, D24, G40, G41. - A2: json-ld.md said the Work projection derives from "non-tombstoned" assertions, which under the spec's own definition would exclude `deprecated` — but the compiler projects those. Names the real filter. - A8: the `/`-in-locator check is not parse-time; it runs per reference during emission. Attributes each check to its actual stage. - B11: the vocabulary list omitted PROV-O, which now carries the primary mapping relation. - B12: drop the "equivalence" framing, including the diagram label, which excluded half the relation enum. - D24: the dcterms row omitted `description` and `isReplacedBy`. - G40: the `-draft` suffix is on the spec document's version, not on any tag. Adds the release-tag vs spec-version distinction, which was explained nowhere on the site. - G41: "a citable release will be tagged once the core stabilises" read oddly beside 0.1.0 metadata. States the actual position without claiming a tag that does not yet exist. Refs #79 Co-Authored-By: Claude Opus 5 --- src/content/docs/standard/index.md | 10 +++++----- src/content/docs/standard/json-ld.md | 18 +++++++++--------- src/content/docs/standard/versioning.md | 8 +++++--- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/content/docs/standard/index.md b/src/content/docs/standard/index.md index ce50a13..c1c6b75 100644 --- a/src/content/docs/standard/index.md +++ b/src/content/docs/standard/index.md @@ -6,9 +6,9 @@ sidebar: order: 1 --- -TextRefs defines a minimal registry standard for stable, machine-addressable references to texts. Its centre is the separation of **identity** from **location**: a reference such as `John.3.16` is one abstract, language-independent identity, while the translations, editions, and providers that carry it are recorded as locations embedded on the reference. `Work` and `CitationSystem` records use flat stable keys; richer bibliographic and authority data is connected through mappings to external systems, with optional lightweight `Work.creators` retained for citation rendering. The model has four registry object types — `Work`, `CitationSystem`, and `CanonicalReference` for identity, plus `MappingAssertion` for equivalence. Locations are recorded as `resolver_targets` entries embedded on each `CanonicalReference`; they are not a separate object type. TextRefs never hosts full text, apparatus, commentary, or copyrighted edition content. +TextRefs defines a minimal registry standard for stable, machine-addressable references to texts. Its centre is the separation of **identity** from **location**: a reference such as `John.3.16` is one abstract, language-independent identity, while the translations, editions, and providers that carry it are recorded as locations embedded on the reference. `Work` and `CitationSystem` records use flat stable keys; richer bibliographic and authority data is connected through mappings to external systems, with optional lightweight `Work.creators` retained for citation rendering. The model has four registry object types — `Work`, `CitationSystem`, and `CanonicalReference` for identity, plus `MappingAssertion` for curated relations to external identifiers. Locations are recorded as `resolver_targets` entries embedded on each `CanonicalReference`; they are not a separate object type. TextRefs never hosts full text, apparatus, commentary, or copyrighted edition content. -One identity fans out to many locations and equivalences — adding a translation adds a resolver-target entry to the reference, never a new reference: +One identity fans out to many locations and mappings — adding a translation adds a resolver-target entry to the reference, never a new reference: ```mermaid flowchart LR @@ -19,7 +19,7 @@ flowchart LR W --> CR CS --> CR end - subgraph equivalence ["Equivalence — same as"] + subgraph mapping ["Mapping — same as, or about"] MA["MappingAssertion
CTS URN"] end W --> MA @@ -30,9 +30,9 @@ flowchart LR - **[Specification](/standard/specification/)** — the normative document: object model, conformance, validation, and the conformance boundary. - **[Identifier syntax](/standard/identifier-syntax/)** — deterministic UUID v5 generation, namespace, and serialization rules. - **[Citation-system profiles](/standard/system-profiles/)** — how citation systems constrain locators, with the seed Bekker and Stephanus profiles. -- **[JSON-LD context](/standard/json-ld/)** — the context mapping TextRefs records onto SKOS, Dublin Core, and schema.org. +- **[JSON-LD context](/standard/json-ld/)** — the context mapping TextRefs records onto SKOS, PROV-O, Dublin Core, and schema.org. - **[Versioning & data packaging](/standard/versioning/)** — how the spec and the monthly registry exports are versioned and packaged. ## Cite this spec -These pages are the working authority for the standard while `v0.1.0-draft` is being settled. The `v1` JSON-LD context is served at `https://textrefs.org/contexts/v1.jsonld`. A frozen, citable release will be tagged once the core stabilises. +These pages are the working authority for the standard while `v0.1.0-draft` is being settled. The `v1` JSON-LD context is served at `https://textrefs.org/contexts/v1.jsonld`. `v0.1.0` is being prepared as the first citable baseline, cut from this working draft. diff --git a/src/content/docs/standard/json-ld.md b/src/content/docs/standard/json-ld.md index 01b2f8a..582b1a3 100644 --- a/src/content/docs/standard/json-ld.md +++ b/src/content/docs/standard/json-ld.md @@ -16,14 +16,14 @@ https://textrefs.org/contexts/v1.jsonld ## Vocabularies -| Prefix | Namespace | Used for | -| --------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------- | -| `tr` | `https://textrefs.org/ontology#` | TextRefs object types, keys, and TextRefs-specific metadata | -| `skos` | `http://www.w3.org/2004/02/skos/core#` | Labels and schemes (`inScheme`) | -| `dcterms` | `http://purl.org/dc/terms/` | `created`, `modified`, `source`, `language`, `license`, `conformsTo`, `isReferencedBy` mapping relation | -| `prov` | `http://www.w3.org/ns/prov#` | Work ↔ same-entity mapping relation (`alternateOf`) | -| `schema` | `https://schema.org/` | `url`, `provider`, `edition`, `creator`, `familyName`, `givenName`, `name` | -| `xsd` | `http://www.w3.org/2001/XMLSchema#` | `xsd:date` typing for `created` / `modified` / `last_checked` | +| Prefix | Namespace | Used for | +| --------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| `tr` | `https://textrefs.org/ontology#` | TextRefs object types, keys, and TextRefs-specific metadata | +| `skos` | `http://www.w3.org/2004/02/skos/core#` | Labels and schemes (`inScheme`) | +| `dcterms` | `http://purl.org/dc/terms/` | `created`, `modified`, `source`, `language`, `license`, `conformsTo`, `description`, `isReferencedBy` mapping relation, `isReplacedBy` successor link | +| `prov` | `http://www.w3.org/ns/prov#` | Work ↔ same-entity mapping relation (`alternateOf`) | +| `schema` | `https://schema.org/` | `url`, `provider`, `edition`, `creator`, `familyName`, `givenName`, `name` | +| `xsd` | `http://www.w3.org/2001/XMLSchema#` | `xsd:date` typing for `created` / `modified` / `last_checked` | ## Mapping relations @@ -32,7 +32,7 @@ The mapping relations are chosen by what the target is, never by author confiden - `alternateOf` → `prov:alternateOf`, when the target is another entity denoting the same work (e.g. a Wikidata item). - `isReferencedBy` → `dcterms:isReferencedBy`, when the target is a document or page about the work (e.g. a Wikipedia article). -Published `Work` records additionally carry direct `alternateOf` / `isReferencedBy` arrays derived from their non-tombstoned mapping assertions, published as `prov:alternateOf` / `dcterms:isReferencedBy` edges from the work IRI to the mapped identifiers without dereferencing the reified `MappingAssertion` records. The arrays are a read-only projection; the assertion stays authoritative ([Specification §6](/standard/specification/#6-work)). +Published `Work` records additionally carry direct `alternateOf` / `isReferencedBy` arrays derived from mapping assertions that are not `withdrawn` or `blocked`, published as `prov:alternateOf` / `dcterms:isReferencedBy` edges from the work IRI to the mapped identifiers without dereferencing the reified `MappingAssertion` records. The arrays are a read-only projection; the assertion stays authoritative ([Specification §6](/standard/specification/#6-work)). Choose `alternateOf` when the target identifies the same work from a different perspective or at a different level of abstraction; choose `isReferencedBy` when the target describes the work rather than identifying it. See [Specification §10](/standard/specification/#10-mappingassertion). diff --git a/src/content/docs/standard/versioning.md b/src/content/docs/standard/versioning.md index 622466d..ab569d4 100644 --- a/src/content/docs/standard/versioning.md +++ b/src/content/docs/standard/versioning.md @@ -14,6 +14,8 @@ TextRefs versions three things that move at different speeds, and archives each | Registry data | `textrefs/registry` | `vYYYY.MM.N` | TextRefs Registry | | Data-package version | inside `datapackage.json` | SemVer **without** leading `v` | (carried within registry deposit) | +The release tag and the spec document's own version string (e.g. `0.1.0-draft` in `specification.md`) are independent labels, not aliases: the tag marks the site repository's release, the document version marks the spec's own maturity, and the two are not required to read identically. + The site repository couples the spec, JSON-LD context, Zod schemas, and Astro site under a single tag because pre-1.0 the site is the spec's reference rendering; splitting them now would create empty changelogs and confuse Zenodo metadata. Registry data is decoupled — record changes flow on their own cadence — and lives in a separate repository because the [Zenodo–GitHub integration](https://docs.github.com/en/repositories/archiving-a-github-repository/referencing-and-citing-content) mints one concept DOI per repository. The two repositories are cross-linked via `.zenodo.json` `related_identifiers`. The site repository includes `textrefs/registry` as a git submodule at `data/`. The registry uses `main` as its working branch. The site pins a specific registry `main` commit through the submodule pointer, and its compiler builds registry dumps from that pinned content for reproducible site releases. @@ -31,7 +33,7 @@ Each `/standard/*` page carries a `maturity` field in its frontmatter, encoding Transitions: -- `0.x` releases stay `working-draft` regardless of any `-draft` suffix on the tag. +- `0.x` releases stay `working-draft` regardless of any `-draft` suffix on the spec document's version string. - First `1.0.0-rc.1` enters `candidate-recommendation`. - `1.0.0` enters `recommendation`. @@ -87,7 +89,7 @@ Registry identity is permanent once promoted: the IRI of a `Work`, `CitationSyst ### Schema -Tombstones use a status value plus one optional field. The old record stays in the data tree with `status: withdrawn` (or `blocked`, for a rights or policy dispute). If a successor exists, the record carries its IRI in `superseded_by` (`dcterms:isReplacedBy` in the published context); a `deprecated` record may do the same. Consumers follow `superseded_by` to find the successor. `MappingAssertion`s are reserved for genuine work-level equivalence claims and MUST NOT be used for succession links. +Tombstones use a status value plus one optional field. The old record stays in the data tree with `status: withdrawn` (or `blocked`, for a rights or policy dispute). If a successor exists, the record carries its IRI in `superseded_by` (`dcterms:isReplacedBy` in the published context); a `deprecated` record may do the same. Consumers follow `superseded_by` to find the successor. `MappingAssertion`s are reserved for work-level relations to external identifiers and MUST NOT be used for succession links. ### On-disk representation @@ -112,7 +114,7 @@ The compiler enforces these invariants, and fails the build on any violation: 5. An `active` `CanonicalReference` MUST have an `active` `Work` and an `active` `CitationSystem` for its own `citation_system_key`. Works and systems are promoted to `active` before or together with whatever depends on them. 6. An `active` `MappingAssertion` MUST take an `active` `Work` as its `subject`. -Two further checks run when a source file is parsed, before any record is built: a work MUST NOT declare the same citation system twice, and a `locator` MUST NOT contain `/` — the alias grammar below distinguishes its two forms by segment count alone. +Two further checks apply. A work MUST NOT declare the same citation system twice, checked when a source file is parsed, before any record is built. A `locator` MUST NOT contain `/`, checked per reference during emission — the alias grammar below distinguishes its two forms by segment count alone. ### Aliases vs. tombstones From e8bc2915e4fc0c38dc5fdb5bb4721aad1dc5bb9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20M=C3=A4hr?= <14755525+maehr@users.noreply.github.com> Date: Thu, 13 Aug 2026 11:26:24 +0200 Subject: [PATCH 5/8] docs(get-started): correct examples against the registry (#79) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consistency audit findings C13-C19, D20, D21, D23, D25, E27-E31, B11, B12. - C13/C14/C15: the "compiled registry record" blocks did not match what the compiler emits — bare SPDX ids instead of IRIs, `status: active` where every record in `data/` is `draft`, stale `modified` dates, and a John.3.16 example missing its second resolver target and showing an unescaped URL. Each block now matches `dist/dump/` field for field. - C16: wrong Wikidata QID for the Dhammapada in two files. - C17: the Dhammapada provider count was wrong in three places, and in authoring.md contradicted the YAML example directly below it. - C18: the `additional_systems` example used an unregistered citation system, which is a build error, and showed `status: active`. - C19: documented an `examples` key that the strict source schema rejects. - D20: told implementers to read a `mappings` array that no record has. - D21: attributed two nonexistent fields to `CitationSystem`. - D23: `url_by` misses are counted and warned about, not silent. - D25: there is no per-system reference list under `/reg/`. - E27: the edge-case bullet forbade edition-level mapping targets that both the same page and the spec permit. - E28/E29: TEI anchors belong in `resolver_targets`, and the PURL row had `/id/` and `/cite/` the wrong way round. - E30/B12: reading URLs and locators are not "mappings". - E31: "two sources" introduced a five-item list. - B11: add PROV-O to the vocabulary list. Refs #79 Co-Authored-By: Claude Opus 5 --- src/content/docs/get-started/authoring.md | 20 +++++----- src/content/docs/get-started/how-it-works.md | 38 ++++++++++++------- src/content/docs/get-started/index.md | 6 +-- .../mappings-and-resolver-targets.md | 6 +-- .../docs/get-started/related-systems.md | 6 +-- src/content/docs/get-started/url-layout.md | 2 +- src/content/docs/get-started/use-cases.md | 2 +- 7 files changed, 46 insertions(+), 34 deletions(-) diff --git a/src/content/docs/get-started/authoring.md b/src/content/docs/get-started/authoring.md index b0c62b4..561353d 100644 --- a/src/content/docs/get-started/authoring.md +++ b/src/content/docs/get-started/authoring.md @@ -17,11 +17,11 @@ data/ └── systems/{system_key}.yaml # one file per CitationSystem ``` -A `Work` source file declares the work itself, references the citation system it uses as its **preferred** citation system (optionally alongside fallback systems, see [Additional citation systems](#additional-citation-systems-and-reference_status)), lists references, optionally lists resolver templates, and optionally lists work-level mappings. A `CitationSystem` source file declares the locator regex and examples. +A `Work` source file declares the work itself, references the citation system it uses as its **preferred** citation system (optionally alongside fallback systems, see [Additional citation systems](#additional-citation-systems-and-reference_status)), lists references, optionally lists resolver templates, and optionally lists work-level mappings. A `CitationSystem` source file declares the locator regex and a prose description of the locator format. ## A worked example -The Dhammapada has 423 verses across 26 chapters and is hosted on four different reading platforms with four different URL patterns. The whole work, with chapter 1 fully wired up, is roughly 60 lines of YAML: +The Dhammapada has 423 verses across 26 chapters and is hosted on two different reading platforms with two different URL patterns. The whole work, with chapter 1 fully wired up, is roughly 60 lines of YAML: ```yaml # data/works/dhammapada.yaml @@ -36,7 +36,7 @@ citation_system: dhammapada-chapter-verse # the work's PREFERRED citation system mappings: - relation: alternateOf - identifier: 'https://www.wikidata.org/entity/Q220114' + identifier: 'https://www.wikidata.org/entity/Q748878' conforms_to: 'https://www.wikidata.org/' source: manual-curation status: draft @@ -124,7 +124,7 @@ Follow CSL-JSON conventions so citeproc-js / Zotero render correctly. ## How URL templates work -The compiler treats every resolver `url` as an [RFC 6570](https://www.rfc-editor.org/rfc/rfc6570) Level 1 template. Variables are drawn from two sources: +The compiler treats every resolver `url` as an [RFC 6570](https://www.rfc-editor.org/rfc/rfc6570) Level 1 template. Variables are drawn from five sources: 1. **Named capture groups** in the citation system's `locator_regex`. For example, a regex like `^(?\d+)\.(?\d+)$` exposes `{chapter}` and `{verse}` to every template. 2. **Zero-padded variants** of any numeric capture, generated automatically: `{chapter02}`, `{chapter03}`, `{chapter04}`, `{verse02}`, `{verse03}`. Use the padding width that matches the target site's URL. @@ -173,7 +173,7 @@ Some providers use chapter or section names that don't fit a formula (e.g. `01-P 2: 'https://ancient-buddhist-texts.net/Texts-and-Translations/Dhammapada/02-Heedfulness.htm' ``` -The compiler looks up the value of the chosen variable (`chapter`) in the map and uses the matching URL. References for chapters not in the map are silently skipped for this provider — finish the map at your own pace. +The compiler looks up the value of the chosen variable (`chapter`) in the map and uses the matching URL. A chapter not in the map is treated exactly like a missing template variable — the entry is skipped and the compiler warns — so finish the map at your own pace and watch the skipped-entry count. ## When even that isn't enough @@ -330,7 +330,7 @@ The top-level `citation_system:` block is the work's **preferred** system: its r work: key: plato.republic preferred_label: Republic - status: active + status: draft created: 2026-05-31 modified: 2026-05-31 @@ -339,12 +339,14 @@ references: - '514a' additional_systems: - - citation_system: book-chapter # fallback system + - citation_system: book-chapter # fallback system — must already exist as data/systems/book-chapter.yaml reference_status: draft # optional; explicit here for clarity references: - '7.1' ``` +`book-chapter` here is illustrative only; every `citation_system` key, whether at the top level or inside `additional_systems`, must already be registered under `data/systems/`. An unregistered key is a build-time error: the compiler throws `references unknown citation_system "…"` for that work. + The status default is **asymmetric**: the top-level block's `reference_status` defaults to the work's own `status`, but an `additional_systems` block's `reference_status` defaults to `draft` — never to the work's status. Adding a fallback system to an already-active work never silently promotes its references to `active`; each fallback is reviewed on its own. Declaring the same `citation_system` twice for one work — as the preferred system and again under `additional_systems`, or twice within `additional_systems` — is rejected when the source file is parsed. Resolver URL templates stay scoped to their block, because their template variables come from that block's own citation system's `locator_regex` capture groups. @@ -414,10 +416,10 @@ The compiler is deterministic: re-running `compile:data` against unchanged sourc ## What lives where - `/id/work/{key}/` — a Work's canonical landing page (mappings, references, citation systems). A sibling `/id/work/{key}.json` serves the same record as JSON-LD. -- `/id/system/{key}/` — a CitationSystem's canonical landing page (regex, examples, references). Plus `/id/system/{key}.json`. +- `/id/system/{key}/` — a CitationSystem's canonical landing page (description, regex, works using it with reference counts). Plus `/id/system/{key}.json`. - `/id/ref/{uuid}/` — a CanonicalReference page with every resolver URL grouped by language. Plus `/id/ref/{uuid}.json`. - `/id/mapping/{uuid}/` — a MappingAssertion page. Plus `/id/mapping/{uuid}.json`. -- `/reg/` — the human registry browser (filter works and citation systems, then browse paginated reference lists from work/system pages). +- `/reg/` — the human registry browser (filter works and citation systems, then browse paginated reference lists from work pages). - `/cite/{work_key}/{citation_system_key}/{locator}/` — qualified short alias, minted for every reference. - `/cite/{work_key}/{locator}/` — bare short alias, minted only for a work's preferred citation system; it MAY be retargeted if that preference changes. diff --git a/src/content/docs/get-started/how-it-works.md b/src/content/docs/get-started/how-it-works.md index 5aecfc0..ff1b025 100644 --- a/src/content/docs/get-started/how-it-works.md +++ b/src/content/docs/get-started/how-it-works.md @@ -11,7 +11,7 @@ The core move is simple: separate the cited passage from the places where someon ## The four records -TextRefs uses three records for identity and one record for work-level equivalences. Reading locations are embedded directly on the `CanonicalReference`. +TextRefs uses three records for identity and one record for work-level mappings. Reading locations are embedded directly on the `CanonicalReference`. | Record | Question it answers | Example | | -------------------- | --------------------------------- | --------------------------------------------- | @@ -51,18 +51,18 @@ The identity is the Stephanus reference, not any one Greek text, translation, or "edition": "Plato, Republic (Burnet, OCT)", "provider": "Perseus Digital Library", "access": "open", - "license": "CC-BY-SA-3.0", + "license": "https://spdx.org/licenses/CC-BY-SA-3.0", "license_url": "https://www.perseus.tufts.edu/hopper/opensource", "last_checked": "2026-06-03" } ], - "status": "active", + "status": "draft", "created": "2026-05-31", - "modified": "2026-06-03" + "modified": "2026-08-12" } ``` -When reading locations exist, they are embedded on the reference itself, one entry per provider or translation. For example, the compiled registry record for [`new-testament` `John.3.16`](/id/ref/b6438d55-f3f2-5fc7-ab40-4f582f8774c3/) contains a resolver target to STEP Bible: +When reading locations exist, they are embedded on the reference itself, one entry per provider or translation. For example, the compiled registry record for [`new-testament` `John.3.16`](/id/ref/b6438d55-f3f2-5fc7-ab40-4f582f8774c3/) contains resolver targets to STEP Bible and Deutsche Bibelgesellschaft: ```json { @@ -73,22 +73,32 @@ When reading locations exist, they are embedded on the reference itself, one ent "locator": "John.3.16", "resolver_targets": [ { - "url": "https://www.stepbible.org/?q=version=SBLG|reference=John.3.16", + "url": "https://www.stepbible.org/?q=version=SBLG%7Creference=John.3.16", "language": "grc", "edition": "SBL Greek New Testament", "provider": "STEP Bible", "access": "open", - "license": "CC-BY-4.0", - "license_url": "https://sblgnt.com/license/" + "license": "https://spdx.org/licenses/CC-BY-4.0", + "license_url": "https://sblgnt.com/license/", + "last_checked": "2026-06-03" + }, + { + "url": "https://www.die-bibel.de/bibel/NA28/JHN.3/#JHN.3.16", + "language": "grc", + "edition": "Nestle-Aland, Novum Testamentum Graece, 28th edn (NA28)", + "provider": "Deutsche Bibelgesellschaft", + "access": "open", + "license_url": "https://www.die-bibel.de/impressum", + "last_checked": "2026-08-12" } ], - "status": "active", + "status": "draft", "created": "2026-05-31", - "modified": "2026-05-31" + "modified": "2026-08-12" } ``` -Work-level equivalences live in a `MappingAssertion`: +Work-level mappings live in a `MappingAssertion`. This one says the Wikidata item denotes the same work: ```json { @@ -100,9 +110,9 @@ Work-level equivalences live in a `MappingAssertion`: "conforms_to": "https://www.wikidata.org/" }, "source": "manual-curation", - "status": "active", + "status": "draft", "created": "2026-05-31", - "modified": "2026-06-02" + "modified": "2026-08-11" } ``` @@ -123,7 +133,7 @@ For a heavily translated work, many locations can share one reference identity. An English translation, a German translation, a Greek edition, and a library scan can all sit in the `resolver_targets` array on the same reference. Adding a new translation adds another entry, not another canonical reference. -For complete worked examples, see the live [Dhammapada work page](/id/work/dhammapada/) (four providers, two languages, 423 references) or the [Plato _Republic_ work page](/id/work/plato.republic/) (Stephanus pagination). The contributor YAML behind them is documented in [Authoring registry data](/get-started/authoring/). +For complete worked examples, see the live [Dhammapada work page](/id/work/dhammapada/) (three resolver targets across two providers, two languages, 423 references) or the [Plato _Republic_ work page](/id/work/plato.republic/) (Stephanus pagination). The contributor YAML behind them is documented in [Authoring registry data](/get-started/authoring/). Where traditions number passages differently, create separate references under separate citation systems. Do not collapse divergent versification, pagination, or segmentation into one identity. The equivalence between the citation systems themselves is not yet expressible in this version: `MappingAssertion.subject` MUST be a Work IRI, so a system-to-system assertion cannot be authored. A future revision may widen `subject` to admit a `CitationSystem` IRI. diff --git a/src/content/docs/get-started/index.md b/src/content/docs/get-started/index.md index 9d382af..ca3f2c9 100644 --- a/src/content/docs/get-started/index.md +++ b/src/content/docs/get-started/index.md @@ -11,13 +11,13 @@ A passage has one identity. The editions that carry it are many. To software it is invisible. The number lives as plain text in a footnote, dependent on a reader who knows the convention. No tool can resolve it, no link can carry it, no pipeline can index it. A reference that survived four hundred years on paper breaks in a decade online, because the edition behind it sits in a repository the citation cannot reach. -TextRefs closes that gap. Every canonical reference is minted as a persistent identity of its own, a single HTTP URI for the passage a scholar means. Editions, translations, corpora and databases attach to it as curated mappings: a Stephanus locator, a CTS URN, a Wikidata QID, a DOI for the Loeb, the reading URL of the archive that holds the text. The citation becomes the doorway, and everything that carries the passage is reachable through it. +TextRefs closes that gap. Every canonical reference is minted as a persistent identity of its own, a single HTTP URI for the passage a scholar means. Editions, translations, corpora and databases attach to it: identifiers that name the same work — a CTS URN, a Wikidata QID, a DOI for the Loeb — and the reading URL of the archive that holds the text, resolving this one passage. The citation becomes the doorway, and everything that carries the passage is reachable through it. This is the interoperability scholarship has lacked. Every scholar already keeps the map privately. Bekker for the _Metaphysics_, Corcilius for the _De anima_, Rashed for _On Generation and Corruption_. Exact, hard-won, and gone the moment the article closes. TextRefs makes it shared and machine-readable. Oxford and the Loeb, Leipzig and Perseus, Wikidata and VIAF keep their own identifiers, their own homes, their own richer work, joined through the one reference they share. No central host. No redundancy. Authority stays with the institutions that earned it, and the archive that digitised an edition is now one mapping away from every citation of the passage it holds. The division is deliberate. TextRefs holds the reference layer only and nothing else. It hosts no text, replaces no edition, claims no apparatus. The same model covers every field that cites by structure: a Stephanus passage in Plato, a Bekker line in Aristotle, an article in the _Summa_, a chapter and verse in the Vulgate, a fragment in the _Digesta_. Each citation system carries its own locator rules. The registry stays small, persistent and legally reusable, released under CC0 so the data flows into any tool that needs it, curated by scholars through reviewed contributions, governed as non-profit infrastructure, not owned by a press. -Four record types carry the model. `Work`, `CitationSystem`, `CanonicalReference`, and `MappingAssertion` for equivalence, published as JSON-LD against SKOS, Dublin Core and schema.org. Existing systems are layered, never displaced. A DOI still names the edition. A CTS URN still names the passage in Perseus. TextRefs holds the canonical reference they share, and makes it resolve. +Four record types carry the model. `Work`, `CitationSystem`, `CanonicalReference`, and `MappingAssertion` for curated relations, published as JSON-LD against SKOS, Dublin Core, PROV-O, and schema.org. Existing systems are layered, never displaced. A DOI still names the edition. A CTS URN still names the passage in Perseus. TextRefs holds the canonical reference they share, and makes it resolve. [Browse the registry](/reg/). [Read the standard](/standard/). [Bring your corpus in](/get-started/authoring/). @@ -34,7 +34,7 @@ Four record types carry the model. `Work`, `CitationSystem`, `CanonicalReference ## Live examples -- [`/id/work/dhammapada/`](/id/work/dhammapada/) — Dhammapada with three real providers (SuttaCentral in Pali and English, ancient-buddhist-texts.net). +- [`/id/work/dhammapada/`](/id/work/dhammapada/) — Dhammapada with three real resolver entries across two providers (SuttaCentral in Pali and English, ancient-buddhist-texts.net). - [`/id/work/plato.republic/`](/id/work/plato.republic/) — Plato's _Republic_ with Stephanus pagination. - [`/cite/plato.republic/514a`](/cite/plato.republic/514a) — a short alias that redirects to the canonical reference URL. The qualified form, [`/cite/plato.republic/stephanus/514a`](/cite/plato.republic/stephanus/514a), resolves to the same page. - [`/reg/`](/reg/) — the registry browser. diff --git a/src/content/docs/get-started/mappings-and-resolver-targets.md b/src/content/docs/get-started/mappings-and-resolver-targets.md index 93c9a5c..d461a30 100644 --- a/src/content/docs/get-started/mappings-and-resolver-targets.md +++ b/src/content/docs/get-started/mappings-and-resolver-targets.md @@ -19,7 +19,7 @@ Ask what the external thing is doing. | is a URL where a reader can inspect a specific passage | entry in `CanonicalReference.resolver_targets` | | is only an author, institution, subject, or non-textual authority | usually not TextRefs | -A `MappingAssertion` is about work-level equivalence. A `resolver_targets` entry is about dereferencing one passage. +A `MappingAssertion` is about the work as a whole. A `resolver_targets` entry is about dereferencing one passage. ## Use a MappingAssertion for work-level identifiers @@ -40,7 +40,7 @@ Choose the relation by what the target is, not by how confident you feel: use `a "subject": "https://textrefs.org/id/work/dhammapada", "relation": "alternateOf", "target": { - "identifier": "https://www.wikidata.org/entity/Q220114", + "identifier": "https://www.wikidata.org/entity/Q748878", "conforms_to": "https://www.wikidata.org/" }, "source": "manual-curation" @@ -117,7 +117,7 @@ An author's name alone is not a `Work`. For example, "Confucius" is an authority **Divergent numbering.** If two traditions number the same material differently, create separate `CanonicalReference`s under separate `CitationSystem`s. The equivalence between the two citation systems is not yet expressible in this version: `MappingAssertion.subject` MUST be a Work IRI, so a system-to-system assertion cannot be authored. A future revision may widen `subject` to admit a `CitationSystem` IRI. -**Contained-by relationships.** If an identifier points to a whole edition, scan, or digital object rather than the exact passage, it is not a `MappingAssertion` target at all. If it is an alternate presentation of the whole work, model it as `alternateOf`; otherwise it belongs in `resolver_targets`. +**Contained-by relationships.** An identifier for a whole edition, scan, or digital object is still chosen by what it denotes, per the [ADR-0006](https://github.com/textrefs/textrefs.org/blob/main/decisions/ADR-0006-mapping-relation-vocabulary.md) test: if it denotes the same work, model it as `alternateOf`; if it is a document about the work, model it as `isReferencedBy`; if it only lets a reader inspect one specific passage, it belongs in `resolver_targets`, not a mapping. **Unstable websites.** A website URL can be useful as a resolver target even if it is not a stable identifier. Do not derive TextRefs IDs from it. diff --git a/src/content/docs/get-started/related-systems.md b/src/content/docs/get-started/related-systems.md index 7eef875..55092a3 100644 --- a/src/content/docs/get-started/related-systems.md +++ b/src/content/docs/get-started/related-systems.md @@ -18,12 +18,12 @@ For the practical modelling distinction between external identifiers and reading | **[DOI](https://www.doi.org/)** | a published digital object (article, edition, dataset) | publication | TextRefs records carry DOI mappings for editions hosting the cited passage | | **[Handle](https://www.handle.net/)** | any digital object with a Handle.net record | object | underlying tech for DOI; same mapping pattern as DOI | | **[ARK](https://arks.org/)** | any object an institution chooses to persist | object | similar role to Handle; same mapping pattern | -| **[PURL](https://purl.archive.org/)** | a redirecting persistent URL | URL only | TextRefs IDs are themselves HTTP URIs that redirect; PURLs can target a TextRefs ID | +| **[PURL](https://purl.archive.org/)** | a redirecting persistent URL | URL only | TextRefs `/id/` URLs are the stable target and do not redirect (`/cite/` does); PURLs can target a TextRefs ID | | **[CTS URN](http://cts.informatik.uni-leipzig.de/Canonical_Text_Service.html)** | a passage in a canonical work, Perseus/Homer-Multitext model | passage | strongest semantic overlap; TextRefs records CTS URNs as mappings so CTS-aware tools can interoperate | | **[DTS API](https://distributed-text-services.github.io/specifications/)** | a discovery/retrieval API for texts using CTS-style URNs | service | downstream consumer — DTS implementations can resolve TextRefs IDs via mappings | | **[Wikidata QID](https://www.wikidata.org/)** | an abstract entity (work, person, event) | work / entity | `Work` records map to Wikidata QIDs; TextRefs IDs handle the references inside those works | | **[VIAF](https://viaf.org/)** | author and work authority records | work / person | mapping target for `Work` records; TextRefs IDs provide passage-level identity | -| **[TEI `xml:id`](https://tei-c.org/)** | a local anchor inside a TEI document | edition-local | edition-internal; TextRefs `MappingAssertion`s can point at a specific TEI anchor in a published edition | +| **[TEI `xml:id`](https://tei-c.org/)** | a local anchor inside a TEI document | edition-local | edition-internal; a TEI anchor for a specific passage belongs in `resolver_targets`, not a `MappingAssertion` | | **Perseus / Scaife URLs** | a passage on a specific reading platform | platform-bound passage | surface as `resolver_targets` entries with provenance so readers can jump from a stable reference to a useful platform | ## Where DOIs fit @@ -36,6 +36,6 @@ For the practical modelling distinction between external identifiers and reading ## What this means for implementers - Treat TextRefs IDs as the **primary** identifier for a canonical reference. -- Read external identifiers and resolver targets from the `mappings` array — they are enriched metadata, not the citation's identity. +- Read external identifiers from a `Work`'s `alternateOf` and `isReferencedBy` arrays, and reading URLs from `CanonicalReference.resolver_targets` — they are enriched metadata, not the citation's identity. - Fall back to your own resolver chain: if no mapping exists for the user's preferred edition, link to a default mapping or to the TextRefs landing page. - When you publish your own data, attach a TextRefs ID alongside whatever you already issue. This is how the citation graph grows without anyone changing primary keys. diff --git a/src/content/docs/get-started/url-layout.md b/src/content/docs/get-started/url-layout.md index 6a7b3a3..5122312 100644 --- a/src/content/docs/get-started/url-layout.md +++ b/src/content/docs/get-started/url-layout.md @@ -10,7 +10,7 @@ TextRefs uses four URL prefixes, each with one job. Together they make every reg | Prefix | Role | What lives there | | -------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `/id/` | **Identifier** | The canonical, persistent URL of every record. Each record is published twice: `/id/.../` (HTML) and a sibling `/id/....json` (JSON-LD). | -| `/reg/` | **Browse** | The human registry browser: filter works and citation systems, then browse paginated reference lists from those record pages. Links into `/id/`. | +| `/reg/` | **Browse** | The human registry browser: filter works and citation systems, then browse paginated reference lists from work pages. Links into `/id/`. | | `/cite/` | **Cite** | Short, memorable URLs (`/cite/{work}/{system}/{locator}` always; bare `/cite/{work}/{locator}` for the work's preferred system) that redirect to the canonical `/id/` URL. Convenience only — bare aliases MAY be retargeted; `/id/` is permanent. | | `/api/` | **API docs** | The OpenAPI document describing the `/id/` URL contract, plus the JSON-LD `@context` at `/contexts/`. | diff --git a/src/content/docs/get-started/use-cases.md b/src/content/docs/get-started/use-cases.md index ac4d86f..ea380c9 100644 --- a/src/content/docs/get-started/use-cases.md +++ b/src/content/docs/get-started/use-cases.md @@ -29,7 +29,7 @@ These scenarios are illustrative, not exhaustive. Each pairs "what happens today **Today.** Stephanus pagination, Bekker numbering, Homeric book-and-line references, and biblical book-chapter-verse — every tradition has its own implicit, untyped notation. There is no machine-readable contract for what is a valid citation in each system. -**With TextRefs.** Each tradition is a `CitationSystem` with a documented locator regex, a normalisation rule, and a list of valid reference types. A parser can validate "Vulg. Gen. 1:1" or reject "ST I-II.300.99" because no registered canonical reference exists for that locator. The reference identifier is independent of any single edition. +**With TextRefs.** Each tradition is a `CitationSystem` with a documented `locator_regex` and a `description` that spells out the canonical locator form. A parser can validate "Vulg. Gen. 1:1" or reject "ST I-II.300.99" because no registered canonical reference exists for that locator. The reference identifier is independent of any single edition. ## AI/LLM grounding and retrieval From 95a2e8b4ad4235a533dd5afbc1e4c61723a1badf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20M=C3=A4hr?= <14755525+maehr@users.noreply.github.com> Date: Thu, 13 Aug 2026 11:39:53 +0200 Subject: [PATCH 6/8] docs(get-started): plain-language pass (#79) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Readability pass over the seven get-started pages, inspired by the rule intent of ASD-STE100 (Simplified Technical English). The specification and its Approved Word dictionary are copyrighted and are not used here, so this is an approximation, not a conformance claim. Applied: one thought per sentence, active voice with a named actor, simple present, noun clusters of at most three words, one term per concept, and paragraphs of at most six sentences. Long enumerating sentences became bulleted lists. Removed "e.g."/"i.e.", slash-as-conjunction, contractions, and semicolons that hid a second sentence — the changes that matter most for non-native readers and for translation. No facts changed. Verified mechanically: - every fenced code block is byte-identical to the previous commit, so the registry values corrected in the preceding commit are untouched; - every heading is unchanged, so no anchor target breaks; - no identifier, URL, count, date, QID, or UUID moved; - RFC 2119 keywords are preserved verbatim. `get-started/index.md` is deliberately exempt from the mechanical limits. It is the landing page and its cadence is intentional, so only three genuinely ambiguous sentences changed there — a dangling participle, a four-clause chain, and a verbless fragment. Its fragments and triads are untouched. Refs #79 Co-Authored-By: Claude Opus 5 --- src/content/docs/get-started/authoring.md | 115 +++++++++++------- src/content/docs/get-started/how-it-works.md | 27 ++-- src/content/docs/get-started/index.md | 6 +- .../mappings-and-resolver-targets.md | 52 ++++---- .../docs/get-started/related-systems.md | 36 +++--- src/content/docs/get-started/url-layout.md | 28 ++--- src/content/docs/get-started/use-cases.md | 18 +-- 7 files changed, 159 insertions(+), 123 deletions(-) diff --git a/src/content/docs/get-started/authoring.md b/src/content/docs/get-started/authoring.md index 561353d..d41061d 100644 --- a/src/content/docs/get-started/authoring.md +++ b/src/content/docs/get-started/authoring.md @@ -5,7 +5,9 @@ sidebar: order: 6 --- -Hand-authored YAML lives in the separate [`textrefs/registry`](https://github.com/textrefs/registry) repository, mounted in this repo as a git submodule at `data/` (run `git submodule update --init --recursive` after cloning; see [`CONTRIBUTING.md`](https://github.com/textrefs/textrefs.org/blob/main/CONTRIBUTING.md) for the full workflow). Contributors edit `data/works/{work_key}.yaml` and `data/systems/{system_key}.yaml` there. The compiled registry — Works, CitationSystems, CanonicalReferences, MappingAssertions — is produced by `npm run build:data` from the pinned submodule pointer and written as JSONL resources plus `datapackage.json` under `dist/dump/`. Published dumps are attached to TextRefs Standard/site releases and long-term archived in the [TextRefs Zenodo community](https://zenodo.org/communities/textrefs/) with citable DOIs. +Hand-authored YAML lives in the separate [`textrefs/registry`](https://github.com/textrefs/registry) repository. This repo mounts it as a git submodule at `data/`. After cloning, run `git submodule update --init --recursive`. See [`CONTRIBUTING.md`](https://github.com/textrefs/textrefs.org/blob/main/CONTRIBUTING.md) for the full workflow. Contributors edit `data/works/{work_key}.yaml` and `data/systems/{system_key}.yaml` there. + +`npm run build:data` produces the compiled registry from the pinned submodule pointer. The compiled registry holds Works, CitationSystems, CanonicalReferences, and MappingAssertions. The command writes them as JSONL resources plus `datapackage.json` under `dist/dump/`. Published dumps are attached to TextRefs Standard releases and site releases. They are also archived long-term in the [TextRefs Zenodo community](https://zenodo.org/communities/textrefs/) with citable DOIs. This page documents the YAML format. @@ -17,7 +19,15 @@ data/ └── systems/{system_key}.yaml # one file per CitationSystem ``` -A `Work` source file declares the work itself, references the citation system it uses as its **preferred** citation system (optionally alongside fallback systems, see [Additional citation systems](#additional-citation-systems-and-reference_status)), lists references, optionally lists resolver templates, and optionally lists work-level mappings. A `CitationSystem` source file declares the locator regex and a prose description of the locator format. +A `Work` source file declares: + +- the work itself; +- its preferred citation system, and any fallback systems (see [Additional citation systems](#additional-citation-systems-and-reference_status)); +- its references; +- its resolver templates, which are optional; +- its work-level mappings, which are also optional. + +A `CitationSystem` source file declares the locator regex and a prose description of the locator format. ## A worked example @@ -66,9 +76,9 @@ references: # … ``` -Each reference gets one entry per resolver. The compiler derives `{verseGlobal}` from the citation system's `chapter_sizes` (see [How URL templates work](#how-url-templates-work)), and looks up `url_by.chapter[N]` for providers whose URL structure isn't templatable. +Each reference gets one entry per resolver. The compiler derives `{verseGlobal}` from the citation system's `chapter_sizes`. See [How URL templates work](#how-url-templates-work). For providers whose URL structure is not templatable, the compiler looks up `url_by.chapter[N]` instead. -Every resolver must actually address the cited passage. A URL with no locator variable in it resolves every reference in the work to the same landing page, which is worse than having no resolver at all — the registry says "here is 1.1" and hands the reader a book. +Every resolver must address the cited passage. A URL with no locator variable resolves every reference in the work to the same landing page. That is worse than having no resolver at all: the registry says "here is 1.1" and hands the reader a book. ### Optional: `creators` @@ -90,37 +100,43 @@ work: name: '[Pseudo-]Aristotle' # institutions, collective, or pseudonymous attribution ``` -Omit `creators` entirely for anonymous or non-authored works (e.g. the Dhammapada, the Bible). For attributed-but-disputed works, record the traditional attribution for citation rendering and document uncertainty through mappings or review notes. The field is purely optional; nothing in the registry depends on it. +Omit `creators` entirely for anonymous or non-authored works, for example the Dhammapada or the Bible. For attributed-but-disputed works, record the traditional attribution for citation rendering. Document the uncertainty through mappings or review notes instead. The field is optional. Nothing in the registry depends on it. ## Naming and identity -Get `work.key`, `work.preferred_label`, and `work.creators` right on the first commit — renaming a key after publication is a tombstone event that mints new reference IRIs. +Get `work.key`, `work.preferred_label`, and `work.creators` right on the first commit. Renaming a key after publication is a tombstone event. It mints new reference IRIs. ### `key` -Shape: `{author-slug}.{work-slug}` for attributed works; bare `{work-slug}` for anonymous, collective, or canonical corpora. +The key has two shapes. Attributed works use `{author-slug}.{work-slug}`. Anonymous, collective, or canonical corpora use a bare `{work-slug}`. -- `author-slug` — lowercased family name (or single mononym for antiquity); ASCII-folded; `-` for spaces; no initials. E.g. `homer`, `plato`, `aristotle`, `wittgenstein`, `confucius`, `laozi`, `murasaki-shikibu`. -- `work-slug` — the short form readers actually use: `iliad`, `republic`, `tractatus`, `analects`, `daodejing`. Avoid cryptic initialisms (`eth-nic`) and avoid full Latin titles unless that _is_ the short form. +- `author-slug`: the lowercased family name, or a single mononym for antiquity. ASCII-fold it. Use `-` for spaces. Do not use initials. For example: `homer`, `plato`, `aristotle`, `wittgenstein`, `confucius`, `laozi`, `murasaki-shikibu`. +- `work-slug`: the short form readers use, for example `iliad`, `republic`, `tractatus`, `analects`, `daodejing`. Avoid cryptic initialisms such as `eth-nic`. Avoid full Latin titles unless the Latin title is the short form. - Bare slug for unattributed corpora: `tanakh`, `dhammapada`, `new-testament`, `quran`. -- Multiple works per author with the same short title: disambiguate inside the work-slug, not by promoting the author. E.g. `aristotle.nicomachean-ethics`, `aristotle.eudemian-ethics`. +- If an author has multiple works with the same short title, disambiguate inside the work-slug rather than by promoting the author. For example: `aristotle.nicomachean-ethics`, `aristotle.eudemian-ethics`. ### `preferred_label` -The display title. No parenthetical disambiguator — author goes in `creators`, edition (SBLGNT, OCT, …) goes on the resolver target, alt-names belong in a future `alt_labels` field. +The display title. Do not add a parenthetical disambiguator: + +- the author goes in `creators`; +- the edition, such as SBLGNT or OCT, goes on the resolver target; +- alt-names belong in a future `alt_labels` field. -- Attributed: just the title — `Iliad`, `Republic`, `Tractatus Logico-Philosophicus`. -- Anonymous / collective: the conventional English name — `Tanakh`, `Dhammapada`, `New Testament`. +Use these forms: + +- Attributed works: just the title, for example `Iliad`, `Republic`, `Tractatus Logico-Philosophicus`. +- Anonymous or collective works: the conventional English name, for example `Tanakh`, `Dhammapada`, `New Testament`. ### `creators` -Follow CSL-JSON conventions so citeproc-js / Zotero render correctly. +Follow CSL-JSON conventions so citeproc-js and Zotero render correctly. -- Standard names: `kind: person` with `family` and `given`. E.g. `{ kind: person, family: Wittgenstein, given: Ludwig }`. -- Mononyms (Homer, Plato, Confucius, Laozi, Murasaki Shikibu, …): `kind: person` with `family` only and no `given`. CSL convention for single-name authors; matches Chicago's "Homer, _Iliad_ 1.1." output. -- Anonymous / collective: **omit `creators` entirely**. Don't write a literal "Anonymous" — absence is the correct CSL signal. -- Reserve `kind: literal` for names that genuinely should not decompose: corporate/institutional authors ("World Health Organization") or pseudonymous attribution strings ("[Pseudo-]Aristotle"). -- Attributed-but-disputed (e.g. Laozi for _Daodejing_): record the traditional attribution as `kind: person, family: Laozi`; do not encode the dispute in the name string. TextRefs has no relation for attribution uncertainty in this version. +- Standard names: `kind: person` with `family` and `given`. For example: `{ kind: person, family: Wittgenstein, given: Ludwig }`. +- Mononyms, for example Homer, Plato, Confucius, Laozi, or Murasaki Shikibu: use `kind: person` with `family` only and no `given`. This is the CSL convention for single-name authors. It matches Chicago's output "Homer, _Iliad_ 1.1." +- Anonymous or collective: **omit `creators` entirely**. Do not write a literal "Anonymous". The absence of the field is the correct CSL signal. +- Reserve `kind: literal` for names that should not decompose: corporate or institutional authors, for example "World Health Organization", or pseudonymous attribution strings, for example "[Pseudo-]Aristotle". +- Attributed-but-disputed works, for example Laozi for _Daodejing_: record the traditional attribution as `kind: person, family: Laozi`. Do not encode the dispute in the name string. TextRefs has no relation for attribution uncertainty in this version. ## How URL templates work @@ -128,15 +144,15 @@ The compiler treats every resolver `url` as an [RFC 6570](https://www.rfc-editor 1. **Named capture groups** in the citation system's `locator_regex`. For example, a regex like `^(?\d+)\.(?\d+)$` exposes `{chapter}` and `{verse}` to every template. 2. **Zero-padded variants** of any numeric capture, generated automatically: `{chapter02}`, `{chapter03}`, `{chapter04}`, `{verse02}`, `{verse03}`. Use the padding width that matches the target site's URL. -3. **Roman-numeral variants** of any numeric capture in 1..3999, generated automatically: `{chapterRoman}` produces `I`, `VIII`, `XXVI`. Useful for sites that anchor sections by Roman chapter (e.g. Wikisource's `#I:8` Dhammapada verses). -4. **Cumulative `{verseGlobal}`** — for systems whose locators have numeric `chapter` and `verse` groups _and_ declare `chapter_sizes:` (see below), the compiler exposes a global 1..N verse counter. Useful for single-page resolvers (e.g. SuttaCentral's `/dhp8`, `/dhp102`) whose URLs use one running index across all chapters. -5. **Provider-specific spellings** declared per resolver with `vars:` — see below. +3. **Roman-numeral variants** of any numeric capture in 1..3999, generated automatically: `{chapterRoman}` produces `I`, `VIII`, `XXVI`. These variants are useful for sites that anchor sections by Roman chapter, for example Wikisource's `#I:8` Dhammapada verses. +4. **Cumulative `{verseGlobal}`**: for systems whose locators have numeric `chapter` and `verse` groups and also declare `chapter_sizes:` (see below), the compiler exposes a global 1..N verse counter. This is useful for single-page resolvers, for example SuttaCentral's `/dhp8` and `/dhp102`, whose URLs use one running index across all chapters. +5. **Provider-specific spellings**, declared per resolver with `vars:`. See below. -If a template references a variable that doesn't exist for a given reference, the compiler skips that resolver entry for that reference and warns. Empty `resolver_targets` arrays are valid; references stay registered. +If a template references a variable that does not exist for a given reference, the compiler skips that resolver entry for that reference and warns. Empty `resolver_targets` arrays are valid. References stay registered. ## When a provider spells a locator value differently -A locator carries one canonical vocabulary — for `bible-book-chapter-verse` that is the [OSIS book codes](https://wiki.crosswire.org/OSIS_Book_Abbreviations) (`Gen`, `John`, `1Cor`). Providers do not all agree with it: die-bibel.de addresses the same books with USFM codes (`GEN`, `JHN`, `1CO`). `vars:` declares that translation for one resolver, leaving the rest of the template intact: +A locator carries one canonical vocabulary. For `bible-book-chapter-verse` that is the [OSIS book codes](https://wiki.crosswire.org/OSIS_Book_Abbreviations), for example `Gen`, `John`, `1Cor`. Providers do not all agree with it: die-bibel.de addresses the same books with USFM codes, for example `GEN`, `JHN`, `1CO`. `vars:` declares that translation for one resolver, and leaves the rest of the template intact: ```yaml - provider: Deutsche Bibelgesellschaft @@ -153,15 +169,15 @@ A locator carries one canonical vocabulary — for `bible-book-chapter-verse` th url: 'https://www.die-bibel.de/bibel/NA28/{bookUsfm}.{chapter}/#{bookUsfm}.{chapter}.{verse}' ``` -Each entry takes the value of `from` and looks it up in `map`, binding the result to a new variable usable anywhere in `url` or as the `url_by` key. The name must be new: a `vars` name that shadows a locator capture group is rejected, so `{book}` always means the canonical code no matter which resolver you are reading. +Each entry takes the value of `from` and looks it up in `map`. The result becomes a new variable, usable anywhere in `url` or as the `url_by` key. The name must be new. A `vars` name that shadows a locator capture group is rejected, so `{book}` always means the canonical code, no matter which resolver you read. -A value with no entry in the map is treated exactly like a missing template variable — the entry is skipped and the compiler warns. A hole in a book table therefore shows up in the build output instead of quietly producing a wrong URL, so watch the skipped-entry count when filling one in. +A value with no entry in the map is treated like a missing template variable: the entry is skipped and the compiler warns. A hole in a book table therefore shows up in the build output, instead of quietly producing a wrong URL. Watch the skipped-entry count when you fill one in. -Reach for `vars:` when a provider renames _part_ of the URL; reach for `url_by:` (below) when the whole URL is arbitrary. +Use `vars:` when a provider renames part of the URL. Use `url_by:` (described below) when the whole URL is arbitrary. ## When a URL pattern isn't templatable -Some providers use chapter or section names that don't fit a formula (e.g. `01-Pairs.htm`, `02-Heedfulness.htm`). For those, replace `url:` with a per-key map: +Some providers use chapter or section names that do not fit a formula, for example `01-Pairs.htm` or `02-Heedfulness.htm`. For those providers, replace `url:` with a per-key map: ```yaml - provider: ancient-buddhist-texts.net @@ -173,11 +189,11 @@ Some providers use chapter or section names that don't fit a formula (e.g. `01-P 2: 'https://ancient-buddhist-texts.net/Texts-and-Translations/Dhammapada/02-Heedfulness.htm' ``` -The compiler looks up the value of the chosen variable (`chapter`) in the map and uses the matching URL. A chapter not in the map is treated exactly like a missing template variable — the entry is skipped and the compiler warns — so finish the map at your own pace and watch the skipped-entry count. +The compiler looks up the value of the chosen variable, `chapter`, in the map and uses the matching URL. A chapter not in the map is treated like a missing template variable: the entry is skipped and the compiler warns. Finish the map at your own pace, and watch the skipped-entry count. ## When even that isn't enough -For one-off URLs that don't fit any pattern (the typical case for older citation systems like Stephanus or Bekker pagination), put the URL directly on the reference using `extra_resolvers`: +For one-off URLs that do not fit any pattern, put the URL directly on the reference using `extra_resolvers`. This is the typical case for older citation systems, for example Stephanus or Bekker pagination. ```yaml references: @@ -195,7 +211,7 @@ Top-level `resolvers:` and per-reference `extra_resolvers:` both contribute to t ## Enumerating canonical reference sets -Hand-listing every verse of Genesis or every line of the Iliad is not the right shape for a YAML file. For works whose reference set is regular enough to describe in a few numbers, use `references_range:` instead of (or alongside) `references:`. Each entry is one named expander; the compiler concatenates every expansion with the explicit `references:` list, de-dupes, and validates each generated locator against the citation system's regex. +Hand-listing every verse of Genesis or every line of the Iliad is not the right shape for a YAML file. For works whose reference set is regular enough to describe in a few numbers, use `references_range:` instead of, or alongside, `references:`. Each entry is one named expander. The compiler concatenates every expansion with the explicit `references:` list, removes duplicates, and validates each generated locator against the citation system's regex. ```yaml # 81 references: '1', '2', …, '81' @@ -320,11 +336,11 @@ references_range: page_range: [327, 621] ``` -Multiple `references_range` entries on one work are concatenated. Combine with explicit `references:` entries for one-off locators that don't fit any range. +Multiple `references_range` entries on one work are concatenated. Combine them with explicit `references:` entries for one-off locators that do not fit any range. ## Additional citation systems and reference_status -The top-level `citation_system:` block is the work's **preferred** system: its references also get the bare `/cite/{work_key}/{locator}/` alias, and it is what `Work.preferred_citation_system_key` points at in the compiled record. A work MAY additionally carry `additional_systems:` — a list of fallback blocks, each with its own `citation_system:`, `resolvers:`, `references:`, and `references_range:`, scoped exactly like the top-level block: +The top-level `citation_system:` block is the work's **preferred** system. Its references also get the bare `/cite/{work_key}/{locator}/` alias. It is also what `Work.preferred_citation_system_key` points at in the compiled record. A work MAY carry `additional_systems:` as well: a list of fallback blocks, each with its own `citation_system:`, `resolvers:`, `references:`, and `references_range:`, scoped exactly like the top-level block: ```yaml work: @@ -345,15 +361,17 @@ additional_systems: - '7.1' ``` -`book-chapter` here is illustrative only; every `citation_system` key, whether at the top level or inside `additional_systems`, must already be registered under `data/systems/`. An unregistered key is a build-time error: the compiler throws `references unknown citation_system "…"` for that work. +`book-chapter` here is illustrative only. Every `citation_system` key must already be registered under `data/systems/`, whether at the top level or inside `additional_systems`. An unregistered key is a build-time error: the compiler throws `references unknown citation_system "…"` for that work. + +The status default is **asymmetric**. The top-level block's `reference_status` defaults to the work's own `status`. An `additional_systems` block's `reference_status` defaults to `draft` instead, never to the work's status. Adding a fallback system to an already-active work never silently promotes its references to `active`. Each fallback is reviewed on its own. -The status default is **asymmetric**: the top-level block's `reference_status` defaults to the work's own `status`, but an `additional_systems` block's `reference_status` defaults to `draft` — never to the work's status. Adding a fallback system to an already-active work never silently promotes its references to `active`; each fallback is reviewed on its own. Declaring the same `citation_system` twice for one work — as the preferred system and again under `additional_systems`, or twice within `additional_systems` — is rejected when the source file is parsed. +Declaring the same `citation_system` twice for one work is rejected when the source file is parsed. This applies whether it appears as the preferred system and again under `additional_systems`, or twice within `additional_systems`. -Resolver URL templates stay scoped to their block, because their template variables come from that block's own citation system's `locator_regex` capture groups. +Resolver URL templates stay scoped to their own block. Their template variables come from the `locator_regex` capture groups of that block's citation system. ## Citation system files -A citation system declares its locator format once and is reused by every work that cites it. +A citation system declares its locator format once. Every work that cites it reuses that declaration. ```yaml # data/systems/dhammapada-chapter-verse.yaml @@ -400,7 +418,10 @@ created: 2026-05-31 modified: 2026-06-01 ``` -Name your capture groups deliberately — every URL template in every work that uses this system can refer to them. Add `chapter_sizes:` only when (a) the locator has numeric `chapter` and `verse` groups and (b) at least one resolver actually needs the global counter. +Name your capture groups deliberately. Every URL template in every work that uses this system can refer to them. Add `chapter_sizes:` only when both conditions hold: + +- the locator has numeric `chapter` and `verse` groups; +- at least one resolver needs the global counter. ## Building, validating, and previewing @@ -411,16 +432,16 @@ npm run build:data # both, in order npm run dev # browse at http://localhost:4321/reg/ ; records live under /id/ ``` -The compiler is deterministic: re-running `compile:data` against unchanged source produces zero diff. `MappingAssertion` and `CanonicalReference` UUIDs are derived from content per [Identifier syntax](/standard/identifier-syntax/), so the same YAML always produces the same identifiers. +The compiler is deterministic. Re-running `compile:data` against unchanged source produces zero diff. `MappingAssertion` and `CanonicalReference` UUIDs are derived from content, per [Identifier syntax](/standard/identifier-syntax/). The same YAML therefore always produces the same identifiers. ## What lives where -- `/id/work/{key}/` — a Work's canonical landing page (mappings, references, citation systems). A sibling `/id/work/{key}.json` serves the same record as JSON-LD. -- `/id/system/{key}/` — a CitationSystem's canonical landing page (description, regex, works using it with reference counts). Plus `/id/system/{key}.json`. -- `/id/ref/{uuid}/` — a CanonicalReference page with every resolver URL grouped by language. Plus `/id/ref/{uuid}.json`. -- `/id/mapping/{uuid}/` — a MappingAssertion page. Plus `/id/mapping/{uuid}.json`. -- `/reg/` — the human registry browser (filter works and citation systems, then browse paginated reference lists from work pages). -- `/cite/{work_key}/{citation_system_key}/{locator}/` — qualified short alias, minted for every reference. -- `/cite/{work_key}/{locator}/` — bare short alias, minted only for a work's preferred citation system; it MAY be retargeted if that preference changes. +- `/id/work/{key}/`: a Work's canonical landing page (mappings, references, citation systems). A sibling `/id/work/{key}.json` serves the same record as JSON-LD. +- `/id/system/{key}/`: a CitationSystem's canonical landing page (description, regex, works using it with reference counts). Plus `/id/system/{key}.json`. +- `/id/ref/{uuid}/`: a CanonicalReference page with every resolver URL grouped by language. Plus `/id/ref/{uuid}.json`. +- `/id/mapping/{uuid}/`: a MappingAssertion page. Plus `/id/mapping/{uuid}.json`. +- `/reg/`: the human registry browser (filter works and citation systems, then browse paginated reference lists from work pages). +- `/cite/{work_key}/{citation_system_key}/{locator}/`: qualified short alias, minted for every reference. +- `/cite/{work_key}/{locator}/`: a bare short alias, minted only for a work's preferred citation system. It MAY be retargeted if that preference changes. -A reader who types `https://textrefs.org/cite/plato.republic/stephanus/514a` (qualified) or `https://textrefs.org/cite/plato.republic/514a` (bare) lands on the same canonical reference page; the alias index is generated alongside the records by the compiler. See [URL layout](/get-started/url-layout/) for the full four-prefix model and alias-permanence rules. +A reader who types `https://textrefs.org/cite/plato.republic/stephanus/514a` (qualified) or `https://textrefs.org/cite/plato.republic/514a` (bare) lands on the same canonical reference page. The compiler generates the alias index alongside the records. See [URL layout](/get-started/url-layout/) for the full four-prefix model and alias-permanence rules. diff --git a/src/content/docs/get-started/how-it-works.md b/src/content/docs/get-started/how-it-works.md index ff1b025..1943637 100644 --- a/src/content/docs/get-started/how-it-works.md +++ b/src/content/docs/get-started/how-it-works.md @@ -20,7 +20,7 @@ TextRefs uses three records for identity and one record for work-level mappings. | `CanonicalReference` | Which point inside the work? | `514a`, with embedded `resolver_targets` | | `MappingAssertion` | What else identifies this _work_? | a CTS URN, Wikidata item, or DOI for the work | -`MappingAssertion.subject` is always a `Work` IRI. Passage-level external identifiers are derived from the work-level mapping plus the locator; they are not stored one per passage. See [the specification](/standard/specification/) for the normative rules. +`MappingAssertion.subject` is always a `Work` IRI. Passage-level external identifiers are derived from the work-level mapping plus the locator. The registry does not store them one per passage. See [the specification](/standard/specification/) for the normative rules. ## From citation string to TextRefs ID @@ -31,7 +31,12 @@ For a citation such as "Plato, _Republic_ 514a", a registry contributor or parse 3. Normalize the locator according to that citation system: `514a`. 4. Mint the `CanonicalReference` ID from the tuple `work_key`, `citation_system_key`, and `locator`. -The result is a persistent TextRefs URI for the cited point itself. That URI stays stable if a website changes its URLs, a library adds a new scan, a translation appears, or a mapping is corrected. +The result is a persistent TextRefs URI for the cited point itself. That URI stays stable even when: + +- a website changes its URLs; +- a library adds a new scan; +- a translation appears; +- a mapping is corrected. ## Example: Plato, Republic 514a @@ -62,7 +67,7 @@ The identity is the Stephanus reference, not any one Greek text, translation, or } ``` -When reading locations exist, they are embedded on the reference itself, one entry per provider or translation. For example, the compiled registry record for [`new-testament` `John.3.16`](/id/ref/b6438d55-f3f2-5fc7-ab40-4f582f8774c3/) contains resolver targets to STEP Bible and Deutsche Bibelgesellschaft: +When reading locations exist, the registry embeds them on the reference itself, one entry per provider or translation. For example, the compiled registry record for [`new-testament` `John.3.16`](/id/ref/b6438d55-f3f2-5fc7-ab40-4f582f8774c3/) contains resolver targets to STEP Bible and Deutsche Bibelgesellschaft: ```json { @@ -116,7 +121,7 @@ Work-level mappings live in a `MappingAssertion`. This one says the Wikidata ite } ``` -Adding a resolver target adds one entry to `resolver_targets`; adding a Wikidata QID adds one `MappingAssertion`. No new records are minted per passage. A reference with no curated reading URL yet is still a valid identity record. +Adding a resolver target adds one entry to `resolver_targets`. Adding a Wikidata QID adds one `MappingAssertion`. The compiler mints no new records per passage. A reference with no curated reading URL yet is still a valid identity record. ## Example: John.3.16 @@ -133,19 +138,25 @@ For a heavily translated work, many locations can share one reference identity. An English translation, a German translation, a Greek edition, and a library scan can all sit in the `resolver_targets` array on the same reference. Adding a new translation adds another entry, not another canonical reference. -For complete worked examples, see the live [Dhammapada work page](/id/work/dhammapada/) (three resolver targets across two providers, two languages, 423 references) or the [Plato _Republic_ work page](/id/work/plato.republic/) (Stephanus pagination). The contributor YAML behind them is documented in [Authoring registry data](/get-started/authoring/). +For complete worked examples, see the live [Dhammapada work page](/id/work/dhammapada/), which has three resolver targets across two providers, two languages, and 423 references. Alternatively, see the [Plato _Republic_ work page](/id/work/plato.republic/), which uses Stephanus pagination. The contributor YAML behind them is documented in [Authoring registry data](/get-started/authoring/). -Where traditions number passages differently, create separate references under separate citation systems. Do not collapse divergent versification, pagination, or segmentation into one identity. The equivalence between the citation systems themselves is not yet expressible in this version: `MappingAssertion.subject` MUST be a Work IRI, so a system-to-system assertion cannot be authored. A future revision may widen `subject` to admit a `CitationSystem` IRI. +Where traditions number passages differently, create separate references under separate citation systems. Do not collapse divergent versification, pagination, or segmentation into one identity. The equivalence between the citation systems themselves is not yet expressible in this version. `MappingAssertion.subject` MUST be a Work IRI, so a system-to-system assertion cannot be authored. A future revision may widen `subject` to admit a `CitationSystem` IRI. ## What TextRefs does not store -TextRefs stores reference data, not texts. Registry records must not include full text, translations, critical apparatus, commentary, or copyrighted edition content. +TextRefs stores reference data, not texts. Registry records must not include: + +- full text; +- translations; +- critical apparatus; +- commentary; +- copyrighted edition content. Keep those in editions, libraries, repositories, or reading platforms. TextRefs only records the stable reference identity, curated mappings, resolver targets, and provenance needed to connect those systems. ## Keep reading -- [Authoring registry data](/get-started/authoring/) — the contributor YAML format and the `npm run build:data` pipeline. +- [Authoring registry data](/get-started/authoring/) documents the contributor YAML format and the `npm run build:data` pipeline. - [Mappings and resolver targets](/get-started/mappings-and-resolver-targets/) explains how to decide whether an external resource should be modelled as a `MappingAssertion` or a resolver-target entry. - [Related identifier systems](/get-started/related-systems/) compares TextRefs with DOI, ARK, CTS, DTS, Wikidata, VIAF, TEI, and platform URLs. - [The standard](/standard/) contains the normative object model and validation rules. diff --git a/src/content/docs/get-started/index.md b/src/content/docs/get-started/index.md index ca3f2c9..442d033 100644 --- a/src/content/docs/get-started/index.md +++ b/src/content/docs/get-started/index.md @@ -11,13 +11,13 @@ A passage has one identity. The editions that carry it are many. To software it is invisible. The number lives as plain text in a footnote, dependent on a reader who knows the convention. No tool can resolve it, no link can carry it, no pipeline can index it. A reference that survived four hundred years on paper breaks in a decade online, because the edition behind it sits in a repository the citation cannot reach. -TextRefs closes that gap. Every canonical reference is minted as a persistent identity of its own, a single HTTP URI for the passage a scholar means. Editions, translations, corpora and databases attach to it: identifiers that name the same work — a CTS URN, a Wikidata QID, a DOI for the Loeb — and the reading URL of the archive that holds the text, resolving this one passage. The citation becomes the doorway, and everything that carries the passage is reachable through it. +TextRefs closes that gap. Every canonical reference is minted as a persistent identity of its own, a single HTTP URI for the passage a scholar means. Editions, translations, corpora and databases attach to it: identifiers that name the same work — a CTS URN, a Wikidata QID, a DOI for the Loeb — and the reading URL that resolves this one passage in the archive that holds the text. The citation becomes the doorway, and everything that carries the passage is reachable through it. This is the interoperability scholarship has lacked. Every scholar already keeps the map privately. Bekker for the _Metaphysics_, Corcilius for the _De anima_, Rashed for _On Generation and Corruption_. Exact, hard-won, and gone the moment the article closes. TextRefs makes it shared and machine-readable. Oxford and the Loeb, Leipzig and Perseus, Wikidata and VIAF keep their own identifiers, their own homes, their own richer work, joined through the one reference they share. No central host. No redundancy. Authority stays with the institutions that earned it, and the archive that digitised an edition is now one mapping away from every citation of the passage it holds. -The division is deliberate. TextRefs holds the reference layer only and nothing else. It hosts no text, replaces no edition, claims no apparatus. The same model covers every field that cites by structure: a Stephanus passage in Plato, a Bekker line in Aristotle, an article in the _Summa_, a chapter and verse in the Vulgate, a fragment in the _Digesta_. Each citation system carries its own locator rules. The registry stays small, persistent and legally reusable, released under CC0 so the data flows into any tool that needs it, curated by scholars through reviewed contributions, governed as non-profit infrastructure, not owned by a press. +The division is deliberate. TextRefs holds the reference layer only and nothing else. It hosts no text, replaces no edition, claims no apparatus. The same model covers every field that cites by structure: a Stephanus passage in Plato, a Bekker line in Aristotle, an article in the _Summa_, a chapter and verse in the Vulgate, a fragment in the _Digesta_. Each citation system carries its own locator rules. The registry stays small, persistent and legally reusable. It is released under CC0 so the data flows into any tool that needs it, curated by scholars through reviewed contributions, and governed as non-profit infrastructure, not owned by a press. -Four record types carry the model. `Work`, `CitationSystem`, `CanonicalReference`, and `MappingAssertion` for curated relations, published as JSON-LD against SKOS, Dublin Core, PROV-O, and schema.org. Existing systems are layered, never displaced. A DOI still names the edition. A CTS URN still names the passage in Perseus. TextRefs holds the canonical reference they share, and makes it resolve. +Four record types carry the model: `Work`, `CitationSystem`, `CanonicalReference`, and `MappingAssertion` for curated relations. TextRefs publishes all four as JSON-LD against SKOS, Dublin Core, PROV-O, and schema.org. Existing systems are layered, never displaced. A DOI still names the edition. A CTS URN still names the passage in Perseus. TextRefs holds the canonical reference they share, and makes it resolve. [Browse the registry](/reg/). [Read the standard](/standard/). [Bring your corpus in](/get-started/authoring/). diff --git a/src/content/docs/get-started/mappings-and-resolver-targets.md b/src/content/docs/get-started/mappings-and-resolver-targets.md index d461a30..60737cd 100644 --- a/src/content/docs/get-started/mappings-and-resolver-targets.md +++ b/src/content/docs/get-started/mappings-and-resolver-targets.md @@ -7,7 +7,7 @@ sidebar: This guide helps contributors decide whether an external resource belongs in a `MappingAssertion`, in the embedded `resolver_targets` array on a `CanonicalReference`, or neither. -Use it after you have already identified the `Work`, the `CitationSystem`, and the `CanonicalReference` for the citation itself. For the underlying model, start with [How it works](/get-started/how-it-works/). For the authoring format, see [Authoring registry data](/get-started/authoring/). +Use this guide after you identify the `Work`, the `CitationSystem`, and the `CanonicalReference` for the citation. For the underlying model, start with [How it works](/get-started/how-it-works/). For the authoring format, see [Authoring registry data](/get-started/authoring/). ## Quick rule @@ -19,20 +19,20 @@ Ask what the external thing is doing. | is a URL where a reader can inspect a specific passage | entry in `CanonicalReference.resolver_targets` | | is only an author, institution, subject, or non-textual authority | usually not TextRefs | -A `MappingAssertion` is about the work as a whole. A `resolver_targets` entry is about dereferencing one passage. +A `MappingAssertion` describes the work as a whole. A `resolver_targets` entry lets a reader dereference one specific passage. ## Use a MappingAssertion for work-level identifiers -Create a `MappingAssertion` when another system has an identifier for the _whole work_ that should be connected to a TextRefs `Work`. The `subject` MUST be a Work IRI. +Create a `MappingAssertion` when another system has an identifier for the _whole work_ and you want to connect it to a TextRefs `Work`. The `subject` MUST be a Work IRI. Common mapping targets include: -- Wikidata QIDs for works; -- CTS URNs at the work level (e.g. `urn:cts:greekLit:tlg0059.tlg030`); -- DOIs, Handles, ARKs, PURLs, or URN:NBNs for editions or digital objects; +- Wikidata QIDs for works. +- CTS URNs at the work level, for example `urn:cts:greekLit:tlg0059.tlg030`. +- DOIs, Handles, ARKs, PURLs, or URN:NBNs for editions or digital objects. - another TextRefs Work when two registries need to be aligned. -Choose the relation by what the target is, not by how confident you feel: use `alternateOf` when the target is another entity denoting the same work (e.g. a Wikidata item). Use `isReferencedBy` when the target is a document or page about the work (e.g. a Wikipedia article). +Choose the relation by what the target is, not by how confident you feel. Use `alternateOf` when the target is another entity that denotes the same work, for example a Wikidata item. Use `isReferencedBy` when the target is a document or page about the work, for example a Wikipedia article. ```json { @@ -47,7 +47,7 @@ Choose the relation by what the target is, not by how confident you feel: use `a } ``` -Passage-level external identifiers (e.g. the CTS URN for `John.3.16`) are not stored as records. They are derived from the work-level mapping plus the locator at resolve time. +Passage-level external identifiers, for example the CTS URN for `John.3.16`, are not stored as records. They are derived from the work-level mapping plus the locator at resolve time. ## Add resolver targets for reading locations @@ -55,10 +55,10 @@ A `resolver_targets` entry is a dereferenceable URL where a reader can inspect t Typical resolver targets include: -- a Perseus or Scaife page for a Greek or Latin passage; -- a Wikisource page or section anchor; -- a Bible Gateway, Sefaria, Quran.com, or similar reading URL; -- an institutional repository page for a digitized edition; +- a Perseus or Scaife page for a Greek or Latin passage. +- a Wikisource page or section anchor. +- a Bible Gateway, Sefaria, Quran.com, or similar reading URL. +- an institutional repository page for a digitized edition. - a licensed platform URL, if the access status is recorded honestly. Each entry records what a reader needs to understand the link: `language` (BCP 47), `edition`, `provider`, `access`, `license` when known, and `last_checked` when maintained. @@ -88,11 +88,11 @@ Each entry records what a reader needs to understand the link: `language` (BCP 4 } ``` -Contributors author resolver targets as URL templates, not raw URLs — see [Authoring registry data](/get-started/authoring/). The compiler expands them per reference. Browse the live output at [`/id/work/dhammapada/`](/id/work/dhammapada/) or [`/id/work/plato.republic/`](/id/work/plato.republic/). +Contributors author resolver targets as URL templates, not raw URLs. See [Authoring registry data](/get-started/authoring/) for the format. The compiler expands them per reference. Browse the live output at [`/id/work/dhammapada/`](/id/work/dhammapada/) or [`/id/work/plato.republic/`](/id/work/plato.republic/). ## Good first candidates -The best TextRefs candidates are works with established, edition-independent citation systems. The examples below are illustrative; each actual profile still needs a documented canonical locator form, a `locator_regex`, and review. +The best TextRefs candidates are works with established, edition-independent citation systems. The examples below are illustrative. Each actual profile still needs a documented canonical locator form, a `locator_regex`, and review. | Area | Citation example | Citation system | Notes | | ---------- | ---------------------------------------- | -------------------- | ------------------------------------------------------------- | @@ -107,17 +107,21 @@ The best TextRefs candidates are works with established, edition-independent cit | Classics | Homer, _Iliad_ `1.1` | book and line | Present as `homer.iliad` with `homer-book-line`. | | Classics | Homer, _Odyssey_ `1.426` | book and line | Present as `homer.odyssey` with `homer-book-line`. | -An author's name alone is not a `Work`. For example, "Confucius" is an authority or attribution problem; _Analects_ is the textual work that can receive canonical references. +An author's name alone is not a `Work`. For example, "Confucius" is an authority or attribution problem. _Analects_ is the textual work that can receive canonical references. ## Edge cases -**Edition page numbers.** Page numbers from one printed edition usually belong to that edition. They can support a resolver target or an edition-level mapping, but they should not become a canonical citation system unless the community actually cites the work that way across editions. +**Edition page numbers.** Page numbers from one printed edition usually belong to that edition. They can support a resolver target or an edition-level mapping. They should not become a canonical citation system unless the community cites the work that way across editions. **Translations.** A translation is a resolver target when it lets readers inspect the cited passage. It is not a new canonical reference unless the translation has its own independently cited segmentation. -**Divergent numbering.** If two traditions number the same material differently, create separate `CanonicalReference`s under separate `CitationSystem`s. The equivalence between the two citation systems is not yet expressible in this version: `MappingAssertion.subject` MUST be a Work IRI, so a system-to-system assertion cannot be authored. A future revision may widen `subject` to admit a `CitationSystem` IRI. +**Divergent numbering.** If two traditions number the same material differently, create separate `CanonicalReference`s under separate `CitationSystem`s. The equivalence between the two citation systems is not yet expressible in this version. `MappingAssertion.subject` MUST be a Work IRI, so a system-to-system assertion cannot be authored. A future revision may widen `subject` to admit a `CitationSystem` IRI. -**Contained-by relationships.** An identifier for a whole edition, scan, or digital object is still chosen by what it denotes, per the [ADR-0006](https://github.com/textrefs/textrefs.org/blob/main/decisions/ADR-0006-mapping-relation-vocabulary.md) test: if it denotes the same work, model it as `alternateOf`; if it is a document about the work, model it as `isReferencedBy`; if it only lets a reader inspect one specific passage, it belongs in `resolver_targets`, not a mapping. +**Contained-by relationships.** An identifier for a whole edition, scan, or digital object is still chosen by what it denotes, per the [ADR-0006](https://github.com/textrefs/textrefs.org/blob/main/decisions/ADR-0006-mapping-relation-vocabulary.md) test: + +- if it denotes the same work, model it as `alternateOf`. +- if it is a document about the work, model it as `isReferencedBy`. +- if it only lets a reader inspect one specific passage, it belongs in `resolver_targets`, not a mapping. **Unstable websites.** A website URL can be useful as a resolver target even if it is not a stable identifier. Do not derive TextRefs IDs from it. @@ -125,12 +129,12 @@ An author's name alone is not a `Work`. For example, "Confucius" is an authority Before proposing mappings or resolver targets, check that: -- the cited passage has a clear `Work`, `CitationSystem`, and canonical locator; -- the citation system documents its canonical locator form and declares a `locator_regex`; -- each `MappingAssertion` subject is a Work IRI and its target identifies a textual resource; -- each `relation` matches what the target actually is (`alternateOf` for a same-work entity, `isReferencedBy` for a document about the work); -- each `resolver_targets` entry has a dereferenceable URL and honest access metadata; -- the proposal documents its source or curation basis; +- the cited passage has a clear `Work`, `CitationSystem`, and canonical locator. +- the citation system documents its canonical locator form and declares a `locator_regex`. +- each `MappingAssertion` subject is a Work IRI and its target identifies a textual resource. +- each `relation` matches what the target is (`alternateOf` for a same-work entity, `isReferencedBy` for a document about the work). +- each `resolver_targets` entry has a dereferenceable URL and honest access metadata. +- the proposal documents its source or curation basis. - no full text, translation text, apparatus, or commentary is copied into the registry. See [Contributing](/community/contributing/) for review tracks and pull-request expectations. diff --git a/src/content/docs/get-started/related-systems.md b/src/content/docs/get-started/related-systems.md index 55092a3..8e91535 100644 --- a/src/content/docs/get-started/related-systems.md +++ b/src/content/docs/get-started/related-systems.md @@ -5,7 +5,7 @@ sidebar: order: 5 --- -Use TextRefs for _canonical references inside a work_ — Stephanus 514a, Bekker 983b10, _ST_ I-II.94.2 — and use existing identifier systems for the editions, files, authority records, catalogues, and platforms that carry those references. The difference matters: you can cite Plato's _Republic_ 514a across a dozen editions and centuries without naming any specific edition, while a DOI identifies one published object at a time. +Use TextRefs for _canonical references inside a work_, such as Stephanus 514a, Bekker 983b10, or _ST_ I-II.94.2. Use existing identifier systems for the editions, files, authority records, catalogues, and platforms that carry those references. The difference matters. You can cite Plato's _Republic_ 514a across a dozen editions and centuries without naming any specific edition. A DOI, by contrast, identifies one published object at a time. So the relationship is almost always: TextRefs holds the canonical reference, and the system in the table below is one of its mappings. @@ -13,29 +13,29 @@ For the practical modelling distinction between external identifiers and reading ## Comparison -| System | Identifies | Granularity | TextRefs relationship | -| :------------------------------------------------------------------------------ | :----------------------------------------------------------- | :--------------------- | :--------------------------------------------------------------------------------------------------------------------- | -| **[DOI](https://www.doi.org/)** | a published digital object (article, edition, dataset) | publication | TextRefs records carry DOI mappings for editions hosting the cited passage | -| **[Handle](https://www.handle.net/)** | any digital object with a Handle.net record | object | underlying tech for DOI; same mapping pattern as DOI | -| **[ARK](https://arks.org/)** | any object an institution chooses to persist | object | similar role to Handle; same mapping pattern | -| **[PURL](https://purl.archive.org/)** | a redirecting persistent URL | URL only | TextRefs `/id/` URLs are the stable target and do not redirect (`/cite/` does); PURLs can target a TextRefs ID | -| **[CTS URN](http://cts.informatik.uni-leipzig.de/Canonical_Text_Service.html)** | a passage in a canonical work, Perseus/Homer-Multitext model | passage | strongest semantic overlap; TextRefs records CTS URNs as mappings so CTS-aware tools can interoperate | -| **[DTS API](https://distributed-text-services.github.io/specifications/)** | a discovery/retrieval API for texts using CTS-style URNs | service | downstream consumer — DTS implementations can resolve TextRefs IDs via mappings | -| **[Wikidata QID](https://www.wikidata.org/)** | an abstract entity (work, person, event) | work / entity | `Work` records map to Wikidata QIDs; TextRefs IDs handle the references inside those works | -| **[VIAF](https://viaf.org/)** | author and work authority records | work / person | mapping target for `Work` records; TextRefs IDs provide passage-level identity | -| **[TEI `xml:id`](https://tei-c.org/)** | a local anchor inside a TEI document | edition-local | edition-internal; a TEI anchor for a specific passage belongs in `resolver_targets`, not a `MappingAssertion` | -| **Perseus / Scaife URLs** | a passage on a specific reading platform | platform-bound passage | surface as `resolver_targets` entries with provenance so readers can jump from a stable reference to a useful platform | +| System | Identifies | Granularity | TextRefs relationship | +| :------------------------------------------------------------------------------ | :-------------------------------------------------------------- | :--------------------- | :--------------------------------------------------------------------------------------------------------------------- | +| **[DOI](https://www.doi.org/)** | a published digital object (article, edition, dataset) | publication | TextRefs records carry DOI mappings for editions that host the cited passage | +| **[Handle](https://www.handle.net/)** | any digital object with a Handle.net record | object | underlying technology for DOI. Same mapping pattern as DOI | +| **[ARK](https://arks.org/)** | any object that an institution chooses to persist | object | similar role to Handle. Same mapping pattern | +| **[PURL](https://purl.archive.org/)** | a redirecting persistent URL | URL only | TextRefs `/id/` URLs are the stable target and do not redirect (`/cite/` does). PURLs can target a TextRefs ID | +| **[CTS URN](http://cts.informatik.uni-leipzig.de/Canonical_Text_Service.html)** | a passage in a canonical work, Perseus/Homer-Multitext model | passage | strongest semantic overlap. TextRefs records CTS URNs as mappings, so CTS-aware tools can interoperate | +| **[DTS API](https://distributed-text-services.github.io/specifications/)** | a discovery and retrieval API for texts that use CTS-style URNs | service | downstream consumer — DTS implementations can resolve TextRefs IDs via mappings | +| **[Wikidata QID](https://www.wikidata.org/)** | an abstract entity (work, person, event) | work / entity | `Work` records map to Wikidata QIDs. TextRefs IDs handle the references inside those works | +| **[VIAF](https://viaf.org/)** | author and work authority records | work / person | mapping target for `Work` records. TextRefs IDs provide passage-level identity | +| **[TEI `xml:id`](https://tei-c.org/)** | a local anchor inside a TEI document | edition-local | edition-internal. A TEI anchor for a specific passage belongs in `resolver_targets`, not a `MappingAssertion` | +| **Perseus / Scaife URLs** | a passage on a specific reading platform | platform-bound passage | surface as `resolver_targets` entries with provenance so readers can jump from a stable reference to a useful platform | ## Where DOIs fit - **Use DOIs for publications.** A DOI is the right identifier for a published edition, article, dataset, or digital object. -- **Use TextRefs for cited passages.** Plato's _Republic_ 514a is a canonical reference. The Loeb edition that contains it can have a DOI; the passage gets a TextRefs ID that can map to that DOI-backed edition. -- **Layer identifiers instead of replacing them.** Scholarly tools already understand DOIs for editions. Adding TextRefs IDs for canonical references gives those tools passage-level precision without changing their publication-level identifiers. +- **Use TextRefs for cited passages.** Plato's _Republic_ 514a is a canonical reference. The Loeb edition that contains it can have a DOI. The passage gets a TextRefs ID that can map to that DOI-backed edition. +- **Layer identifiers instead of replacing them.** Scholarly tools already understand DOIs for editions. If you add TextRefs IDs for canonical references, those tools gain passage-level precision, and their publication-level identifiers stay the same. - **Keep the model affordable.** Canonical-reference coverage grows into the millions. A lightweight open registry is the practical way to curate that graph at non-profit scale. ## What this means for implementers - Treat TextRefs IDs as the **primary** identifier for a canonical reference. -- Read external identifiers from a `Work`'s `alternateOf` and `isReferencedBy` arrays, and reading URLs from `CanonicalReference.resolver_targets` — they are enriched metadata, not the citation's identity. -- Fall back to your own resolver chain: if no mapping exists for the user's preferred edition, link to a default mapping or to the TextRefs landing page. -- When you publish your own data, attach a TextRefs ID alongside whatever you already issue. This is how the citation graph grows without anyone changing primary keys. +- Read external identifiers from a `Work`'s `alternateOf` and `isReferencedBy` arrays, and reading URLs from `CanonicalReference.resolver_targets`. These are enriched metadata, not the citation's identity. +- If no mapping exists for the user's preferred edition, fall back to your own resolver chain. Link to a default mapping or to the TextRefs landing page. +- When you publish your own data, attach a TextRefs ID alongside whatever you already issue. This is how the citation graph grows, and no one needs to change primary keys. diff --git a/src/content/docs/get-started/url-layout.md b/src/content/docs/get-started/url-layout.md index 5122312..835a04a 100644 --- a/src/content/docs/get-started/url-layout.md +++ b/src/content/docs/get-started/url-layout.md @@ -7,12 +7,12 @@ sidebar: TextRefs uses four URL prefixes, each with one job. Together they make every registry record citeable, browsable, machine-readable, and short-linkable. -| Prefix | Role | What lives there | -| -------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `/id/` | **Identifier** | The canonical, persistent URL of every record. Each record is published twice: `/id/.../` (HTML) and a sibling `/id/....json` (JSON-LD). | -| `/reg/` | **Browse** | The human registry browser: filter works and citation systems, then browse paginated reference lists from work pages. Links into `/id/`. | -| `/cite/` | **Cite** | Short, memorable URLs (`/cite/{work}/{system}/{locator}` always; bare `/cite/{work}/{locator}` for the work's preferred system) that redirect to the canonical `/id/` URL. Convenience only — bare aliases MAY be retargeted; `/id/` is permanent. | -| `/api/` | **API docs** | The OpenAPI document describing the `/id/` URL contract, plus the JSON-LD `@context` at `/contexts/`. | +| Prefix | Role | What lives there | +| -------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `/id/` | **Identifier** | The canonical, persistent URL of every record. Each record is published twice: `/id/.../` (HTML) and a sibling `/id/....json` (JSON-LD). | +| `/reg/` | **Browse** | The human registry browser: filter works and citation systems, then browse paginated reference lists from work pages. Links into `/id/`. | +| `/cite/` | **Cite** | Short, memorable URLs (`/cite/{work}/{system}/{locator}` always, and bare `/cite/{work}/{locator}` for the work's preferred system) that redirect to the canonical `/id/` URL. Convenience only. Bare aliases MAY be retargeted. `/id/` is permanent. | +| `/api/` | **API docs** | The OpenAPI document that describes the `/id/` URL contract, plus the JSON-LD `@context` at `/contexts/`. | In one line: @@ -27,10 +27,10 @@ Plato's _Republic_ 514a — the Stephanus passage where Socrates begins the Alle - `https://textrefs.org/id/ref/dc799d4b-9b17-5d76-85aa-dfd001c5321d.json` (JSON-LD for machines) - **Browseable index** — the registry's human entry point, where readers find works, citation systems, and (via the work page) every reference: - `https://textrefs.org/reg/` -- **Short alias** — a memorable, hand-typeable shortcut that redirects to the canonical URL. Every reference has a qualified alias; the bare form exists only for the work's preferred citation system: +- **Short alias** — a memorable, hand-typeable shortcut that redirects to the canonical URL. Every reference has a qualified alias. The bare form exists only for the work's preferred citation system: - `https://textrefs.org/cite/plato.republic/stephanus/514a` (qualified — always minted) - `https://textrefs.org/cite/plato.republic/514a` (bare — minted because Stephanus is Republic's preferred system) -- **Machine contract** — the OpenAPI describing how `/id/` behaves, so a client knows it can append `.json` to any canonical URL: +- **Machine contract** — the OpenAPI that describes how `/id/` behaves, so a client knows it can append `.json` to any canonical URL: - `https://textrefs.org/api/` ## How machine clients discover the JSON @@ -45,17 +45,17 @@ There is no `Accept`-header content negotiation. Every HTML record page advertis /> ``` -A client either reads that `` tag, or simply appends `.json` to the canonical URL. The JSON payload carries the JSON-LD `@context` at [`/contexts/v1.jsonld`](/contexts/v1.jsonld) and is valid JSON-LD by content. +A client either reads that `` tag, or appends `.json` to the canonical URL. The JSON payload carries the JSON-LD `@context` at [`/contexts/v1.jsonld`](/contexts/v1.jsonld) and is valid JSON-LD by content. This mirrors how arxiv.org publishes each paper at `/abs/{id}` and `/pdf/{id}` — two static URLs, two representations, no negotiation needed. ## Why four prefixes, not one -Persistent-identifier systems separate concerns. DOI and ORCID each have a canonical resolver URL that _is_ the API, with documentation living at a stable but distinct path. W3ID and PURL add short-alias redirects on top. TextRefs follows the same pattern: +Persistent-identifier systems separate concerns. DOI and ORCID each have a canonical resolver URL that _is_ the API. Their documentation lives at a stable but distinct path. W3ID and PURL add short-alias redirects on top. TextRefs follows the same pattern: -- The identifier (`/id/`) is the contract — it must be persistent and stable across editions, providers, and resolver implementations. -- The browser (`/reg/`) is the discovery surface — it can change shape and add features without breaking citations. -- The alias (`/cite/`) is convenience — short URLs that resolve back to the canonical identifier via ``. The qualified form (`/cite/{work}/{system}/{locator}`) exists for every reference; the bare form (`/cite/{work}/{locator}`) exists only for a work's preferred system and MAY be retargeted if that preference changes — `/id/` never is. -- The docs (`/api/`) describe the contract for anyone integrating against `/id/`. +- The identifier (`/id/`) is the contract. It must be persistent and stable across editions, providers, and resolver implementations. +- The browser (`/reg/`) is the discovery surface. It can change shape and add features, and citations do not break. +- The alias (`/cite/`) is convenience. Short URLs resolve back to the canonical identifier via ``. The qualified form (`/cite/{work}/{system}/{locator}`) exists for every reference. The bare form (`/cite/{work}/{locator}`) exists only for a work's preferred system and MAY be retargeted if that preference changes. `/id/` is never retargeted. +- The docs (`/api/`) describe the contract for anyone who integrates against `/id/`. If you only remember one thing: **cite the `/id/` URL, browse from `/reg/`, share the `/cite/` shortcut, and read `/api/` to integrate.** diff --git a/src/content/docs/get-started/use-cases.md b/src/content/docs/get-started/use-cases.md index ea380c9..c6db2d0 100644 --- a/src/content/docs/get-started/use-cases.md +++ b/src/content/docs/get-started/use-cases.md @@ -9,25 +9,25 @@ These scenarios are illustrative, not exhaustive. Each pairs "what happens today ## Researcher citing a passage across editions -**Today.** You write "Plato, _Rep._ 514a" in your manuscript. Your reader either knows the Stephanus convention and resolves the citation themselves, or they don't. If they want to follow up, they pick whichever edition is at hand; there is no shared link. +**Today.** You write "Plato, _Rep._ 514a" in your manuscript. Your reader either knows the Stephanus convention and resolves the citation themselves, or they don't. If they want to follow up, they pick whichever edition is at hand. There is no shared link. -**With TextRefs.** You drop in `https://textrefs.org/id/ref/...` (or share the short alias `/cite/plato.republic/514a`). The reader resolves it to a landing page listing every curated resolver target — Perseus, Scaife, Loeb (where licensed), a Wikisource transcript — plus a JSON-LD record and downloadable CSL JSON for citation tools. Switching editions is a click, not a search. +**With TextRefs.** You drop in `https://textrefs.org/id/ref/...` (or share the short alias `/cite/plato.republic/514a`). The reader resolves it to a landing page that lists every curated resolver target, for example Perseus, Scaife, Loeb where licensed, and a Wikisource transcript. The page also gives a JSON-LD record and downloadable CSL JSON for citation tools. You can switch editions with a click, not a search. ## Digital edition project linking to and from canonical references -**Today.** You publish a critical edition with project-local IDs. Other projects link to your URLs; six months later you reorganise the site and every inbound link breaks. You email three colleagues and apologise. +**Today.** You publish a critical edition with project-local IDs. Other projects link to your URLs. Six months later you reorganise the site, and every inbound link breaks. You email three colleagues and apologise. -**With TextRefs.** You mint a TextRefs ID for each canonical reference your edition surfaces and link your project-local IDs to it. Inbound scholarly links can target the TextRefs URI; you control your internal URL scheme without breaking the citation graph. +**With TextRefs.** You mint a TextRefs ID for each canonical reference that your edition surfaces, and link your project-local IDs to it. Inbound scholarly links can target the TextRefs URI. You control your internal URL scheme, and the citation graph does not break. ## Library or institutional repository indexing scholarly works -**Today.** Your full-text indexer extracts citations from a PDF — "Aristotle, _Eth. Nic._ 1094a1" — as a string. It can match other strings literally but it can't cluster, can't infer the work, can't expand abbreviations to _Nicomachean Ethics_. +**Today.** Your full-text indexer extracts citations from a PDF — "Aristotle, _Eth. Nic._ 1094a1" — as a string. It can match other strings literally. It cannot cluster them, infer the work, or expand abbreviations to _Nicomachean Ethics_. **With TextRefs.** The indexer parses the citation against the `bekker` `CitationSystem` and stores the resulting TextRefs ID. Now you have FRBR-style work clustering for free, cross-corpus passage search, and authority alignment with Wikidata via mappings. ## Theologian or legal scholar working with traditional reference systems -**Today.** Stephanus pagination, Bekker numbering, Homeric book-and-line references, and biblical book-chapter-verse — every tradition has its own implicit, untyped notation. There is no machine-readable contract for what is a valid citation in each system. +**Today.** Every tradition has its own implicit, untyped notation, for example Stephanus pagination, Bekker numbering, Homeric book-and-line references, or biblical book-chapter-verse. There is no machine-readable contract for what is a valid citation in each system. **With TextRefs.** Each tradition is a `CitationSystem` with a documented `locator_regex` and a `description` that spells out the canonical locator form. A parser can validate "Vulg. Gen. 1:1" or reject "ST I-II.300.99" because no registered canonical reference exists for that locator. The reference identifier is independent of any single edition. @@ -35,10 +35,10 @@ These scenarios are illustrative, not exhaustive. Each pairs "what happens today **Today.** Language models cite "Plato, _Republic_ 514a" verbatim from training data, including hallucinated passages. Retrieval-augmented systems have nothing to retrieve _against_ at the passage level — only documents. -**With TextRefs.** Each canonical reference is a structured anchor. Training pipelines can tag occurrences in source material; retrievers can ground generations against `https://textrefs.org/id/ref/...` URIs; verification tools can compare the model's claim to a known reference and flag drift. +**With TextRefs.** Each canonical reference is a structured anchor. Training pipelines can tag occurrences in source material. Retrievers can ground generations against `https://textrefs.org/id/ref/...` URIs. Verification tools can compare the model's claim to a known reference and flag drift. ## Citation-managing tools and scholarly markdown -**Today.** Citation managers store free-text "Rep. 514a" — searchable, but neither typed nor linkable. Hypothes.is annotations on canonical passages are tied to a specific edition's URL. +**Today.** Citation managers store free-text such as "Rep. 514a". This text is searchable, but it is neither typed nor linkable. Hypothes.is annotations on canonical passages are tied to a specific edition's URL. -**With TextRefs.** Citation managers store a TextRefs URI as the primary key; the human-readable label is just a display string. Annotation tools can normalise edition-bound URLs to TextRefs IDs so a marginal note on Plato 514a follows the passage rather than the edition. +**With TextRefs.** Citation managers store a TextRefs URI as the primary key. The human-readable label is only a display string. Annotation tools can normalise edition-bound URLs to TextRefs IDs, so a marginal note on Plato 514a follows the passage rather than the edition. From 159703af9b0a46b7dc7ec349c3c7c3cee94208c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20M=C3=A4hr?= <14755525+maehr@users.noreply.github.com> Date: Thu, 13 Aug 2026 15:47:47 +0200 Subject: [PATCH 7/8] docs(get-started): group related standards by layer (#81) Reframe "Related identifier systems" as "Related standards and systems". The page grouped every system as an identifier scheme, but the listed systems address different layers of textual reference. Group the comparison into seven sections: canonical reference and text APIs, persistent identifiers, bibliographic and authority models, edition and fragment addressing, digital surrogates, editorial conceptual models, and platforms. Add IIIF Presentation API, W3C Web Annotation, BIBFRAME, IFLA LRM, and RAMEN. Add URN:NBN, which Appendix B of the specification already lists. Update the two inbound links in the get-started section. Co-Authored-By: Claude Opus 5 --- src/content/docs/get-started/how-it-works.md | 2 +- src/content/docs/get-started/index.md | 2 +- .../docs/get-started/related-systems.md | 106 +++++++++++++++--- 3 files changed, 91 insertions(+), 19 deletions(-) diff --git a/src/content/docs/get-started/how-it-works.md b/src/content/docs/get-started/how-it-works.md index 1943637..d53da6b 100644 --- a/src/content/docs/get-started/how-it-works.md +++ b/src/content/docs/get-started/how-it-works.md @@ -158,5 +158,5 @@ Keep those in editions, libraries, repositories, or reading platforms. TextRefs - [Authoring registry data](/get-started/authoring/) documents the contributor YAML format and the `npm run build:data` pipeline. - [Mappings and resolver targets](/get-started/mappings-and-resolver-targets/) explains how to decide whether an external resource should be modelled as a `MappingAssertion` or a resolver-target entry. -- [Related identifier systems](/get-started/related-systems/) compares TextRefs with DOI, ARK, CTS, DTS, Wikidata, VIAF, TEI, and platform URLs. +- [Related standards and systems](/get-started/related-systems/) groups the neighbouring standards by the layer they address, from identifier schemes to annotation models and reading platforms. - [The standard](/standard/) contains the normative object model and validation rules. diff --git a/src/content/docs/get-started/index.md b/src/content/docs/get-started/index.md index 442d033..64d419a 100644 --- a/src/content/docs/get-started/index.md +++ b/src/content/docs/get-started/index.md @@ -27,7 +27,7 @@ Four record types carry the model: `Work`, `CitationSystem`, `CanonicalReference - [Use cases](/get-started/use-cases/) — concrete scenarios across research, libraries, digital editions, and AI grounding. - [Mappings and resolver targets](/get-started/mappings-and-resolver-targets/) — how to model external identifiers, reading URLs, and canonical-citation candidates. - [Authoring registry data](/get-started/authoring/) — the contributor YAML format and the `npm run build:data` pipeline. -- [Related identifier systems](/get-started/related-systems/) — how TextRefs relates to DOI, ARK, CTS, DTS, Wikidata, VIAF, and friends. +- [Related standards and systems](/get-started/related-systems/) — how TextRefs relates to identifier schemes, text APIs, bibliographic models, edition and annotation standards, digital surrogates, and reading platforms. - [URL layout](/get-started/url-layout/) — how `/id/`, `/reg/`, `/cite/`, and `/api/` fit together. - [The standard](/standard/) — the normative specification text (`v0.1.0-draft`). - [The association](/association/) — the non-profit behind TextRefs, its statutes, and the open board search. diff --git a/src/content/docs/get-started/related-systems.md b/src/content/docs/get-started/related-systems.md index 8e91535..ccecbfe 100644 --- a/src/content/docs/get-started/related-systems.md +++ b/src/content/docs/get-started/related-systems.md @@ -1,30 +1,101 @@ --- -title: Related identifier systems -description: How TextRefs relates to DOI, ARK, Handle, PURL, CTS, DTS, Wikidata, VIAF, TEI, and platform URLs. +title: Related standards and systems +description: How TextRefs relates to identifier schemes, text APIs, bibliographic models, edition and annotation standards, digital surrogates, and reading platforms. sidebar: order: 5 --- Use TextRefs for _canonical references inside a work_, such as Stephanus 514a, Bekker 983b10, or _ST_ I-II.94.2. Use existing identifier systems for the editions, files, authority records, catalogues, and platforms that carry those references. The difference matters. You can cite Plato's _Republic_ 514a across a dozen editions and centuries without naming any specific edition. A DOI, by contrast, identifies one published object at a time. -So the relationship is almost always: TextRefs holds the canonical reference, and the system in the table below is one of its mappings. +The systems on this page work at different layers of textual reference. Very few of them compete with TextRefs. Some supply mapping targets for a `Work`. Others describe a different layer: the encoded edition, the digitized object, the annotation, or the bibliographic record. Each section below names one layer and states what TextRefs adds to it. For the practical modelling distinction between external identifiers and reading URLs, see [Mappings and resolver targets](/get-started/mappings-and-resolver-targets/). -## Comparison - -| System | Identifies | Granularity | TextRefs relationship | -| :------------------------------------------------------------------------------ | :-------------------------------------------------------------- | :--------------------- | :--------------------------------------------------------------------------------------------------------------------- | -| **[DOI](https://www.doi.org/)** | a published digital object (article, edition, dataset) | publication | TextRefs records carry DOI mappings for editions that host the cited passage | -| **[Handle](https://www.handle.net/)** | any digital object with a Handle.net record | object | underlying technology for DOI. Same mapping pattern as DOI | -| **[ARK](https://arks.org/)** | any object that an institution chooses to persist | object | similar role to Handle. Same mapping pattern | -| **[PURL](https://purl.archive.org/)** | a redirecting persistent URL | URL only | TextRefs `/id/` URLs are the stable target and do not redirect (`/cite/` does). PURLs can target a TextRefs ID | -| **[CTS URN](http://cts.informatik.uni-leipzig.de/Canonical_Text_Service.html)** | a passage in a canonical work, Perseus/Homer-Multitext model | passage | strongest semantic overlap. TextRefs records CTS URNs as mappings, so CTS-aware tools can interoperate | -| **[DTS API](https://distributed-text-services.github.io/specifications/)** | a discovery and retrieval API for texts that use CTS-style URNs | service | downstream consumer — DTS implementations can resolve TextRefs IDs via mappings | -| **[Wikidata QID](https://www.wikidata.org/)** | an abstract entity (work, person, event) | work / entity | `Work` records map to Wikidata QIDs. TextRefs IDs handle the references inside those works | -| **[VIAF](https://viaf.org/)** | author and work authority records | work / person | mapping target for `Work` records. TextRefs IDs provide passage-level identity | -| **[TEI `xml:id`](https://tei-c.org/)** | a local anchor inside a TEI document | edition-local | edition-internal. A TEI anchor for a specific passage belongs in `resolver_targets`, not a `MappingAssertion` | -| **Perseus / Scaife URLs** | a passage on a specific reading platform | platform-bound passage | surface as `resolver_targets` entries with provenance so readers can jump from a stable reference to a useful platform | +## Canonical reference and text APIs + +These systems address the same layer as TextRefs: the passage inside a work. + +| System | Identifies | Granularity | TextRefs relationship | +| :------------------------------------------------------------------------------ | :-------------------------------------------------------------- | :---------- | :----------------------------------------------------------------------------------------------------- | +| **[CTS URN](http://cts.informatik.uni-leipzig.de/Canonical_Text_Service.html)** | a passage in a canonical work, Perseus/Homer-Multitext model | passage | strongest semantic overlap. TextRefs records CTS URNs as mappings, so CTS-aware tools can interoperate | +| **[DTS API](https://distributed-text-services.github.io/specifications/)** | a discovery and retrieval API for texts that use CTS-style URNs | service | downstream consumer — DTS implementations can resolve TextRefs IDs via mappings | + +## Persistent object and publication identifiers + +These schemes name a published object: an edition, an article, a dataset, or a scan. A TextRefs record maps to them at work level. The standard lists the same schemes with example `conforms_to` values in [Appendix B of the specification](/standard/specification/#appendix-b-well-known-external-identifier-schemes-informative). + +| System | Identifies | Granularity | TextRefs relationship | +| :---------------------------------------- | :----------------------------------------------------- | :---------- | :------------------------------------------------------------------------------------------------------------- | +| **[DOI](https://www.doi.org/)** | a published digital object (article, edition, dataset) | publication | TextRefs records carry DOI mappings for editions that host the cited passage | +| **[Handle](https://www.handle.net/)** | any digital object with a Handle.net record | object | underlying technology for DOI. Same mapping pattern as DOI | +| **[ARK](https://arks.org/)** | any object that an institution chooses to persist | object | similar role to Handle. Same mapping pattern | +| **[PURL](https://purl.archive.org/)** | a redirecting persistent URL | URL only | TextRefs `/id/` URLs are the stable target and do not redirect (`/cite/` does). PURLs can target a TextRefs ID | +| **[URN:NBN](https://nbn-resolving.org/)** | a digital object registered by a national library | object | national-library counterpart to Handle and ARK. Same mapping pattern | + +## Bibliographic and authority models + +These models describe works, agents, and records in libraries. Wikidata and VIAF supply mapping targets today. BIBFRAME and IFLA LRM show where TextRefs sits relative to library metadata. + +| System | Identifies | Granularity | TextRefs relationship | +| :----------------------------------------------------------------- | :------------------------------------------------- | :-------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------- | +| **[Wikidata QID](https://www.wikidata.org/)** | an abstract entity (work, person, event) | work / entity | `Work` records map to Wikidata QIDs. TextRefs IDs handle the references inside those works | +| **[VIAF](https://viaf.org/)** | author and work authority records | work / person | mapping target for `Work` records. TextRefs IDs provide passage-level identity | +| **[BIBFRAME](https://www.loc.gov/bibframe/)** | a bibliographic description (Work, Instance, Item) | work / instance | complementary vocabulary. Its Work level sits near a TextRefs `Work`; its Instance level is what a DOI or an ARK names | +| **[IFLA LRM](https://repository.ifla.org/handle/20.500.14598/40)** | a conceptual model for bibliographic information | model | no identifiers of its own. Its work-to-manifestation split is the split TextRefs makes between a canonical reference and the editions that carry it | + +## Edition and fragment addressing + +These standards point into one representation of a text: one encoded file, one rendering, one fragment. + +| System | Identifies | Granularity | TextRefs relationship | +| :---------------------------------------------------------------- | :--------------------------------------------------------- | :------------------------------- | :------------------------------------------------------------------------------------------------------------ | +| **[TEI `xml:id`](https://tei-c.org/)** | a local anchor inside a TEI document | edition-local | edition-internal. A TEI anchor for a specific passage belongs in `resolver_targets`, not a `MappingAssertion` | +| **[W3C Web Annotation](https://www.w3.org/TR/annotation-model/)** | an annotation, and the target it anchors through selectors | representation-specific fragment | complementary. Use a TextRefs URI as the annotation target for the canonical passage | + +The two carry different claims: + +- A **TextRefs URI** is the canonical semantic reference. It holds no matter which edition, file, or rendering you open. +- A **Web Annotation selector** is a representation-specific location. It holds for the one text it was anchored to. + +IIIF builds on the Web Annotation model, so the same division applies to the next section. + +## Digital surrogates + +IIIF describes the digitized object and its parts, not the canonical passage. + +| System | Identifies | Granularity | TextRefs relationship | +| :----------------------------------------------------------------- | :--------------------------------------------------------- | :---------------------- | :--------------------------------------------------------------------------------------------------------------- | +| **[IIIF Presentation API](https://iiif.io/api/presentation/3.0/)** | a digitized object and its parts (Manifest, Canvas, Range) | digital object / canvas | complementary. A IIIF resource that shows one passage belongs in `resolver_targets`, not in a `MappingAssertion` | + +The questions differ: + +- **TextRefs** answers: which canonical passage is this? +- **IIIF** answers: where does that passage appear in this digitized edition? + +TextRefs already borrows one convention from this layer. [ADR-0001](https://github.com/textrefs/textrefs.org/blob/main/decisions/ADR-0001-conforms-to-replaces-target-kind.md) adopted it from the Linked Art digital integration model, where the value points at a specification such as the IIIF profile URI. + +## Editorial conceptual models + +RAMEN models editorial objects and their relations. It is a recent model, and its own documentation states that it does not replace TEI or IIIF. + +| System | Identifies | Granularity | TextRefs relationship | +| :--------------------------------------------- | :---------------------------------------------------------- | :---------- | :----------------------------------------------------------------------------------------------------------------- | +| **[RAMEN](https://ramen-schema.org/concepts)** | editorial objects (Collection, Content, Annotation, Entity) | model | complementary. TextRefs can serve as the canonical-reference layer that a RAMEN `Annotation` or `Entity` refers to | + +That gives each layer one job: + +- **RAMEN** — editorial objects and their relations. +- **TEI** — textual representation and encoding. +- **IIIF** — digital objects and their presentation. +- **TextRefs** — stable canonical references. + +## Platforms and resolvers + +A reading platform shows the passage. It does not identify the passage. + +| System | Identifies | Granularity | TextRefs relationship | +| :------------------------ | :--------------------------------------- | :--------------------- | :--------------------------------------------------------------------------------------------------------------------- | +| **Perseus / Scaife URLs** | a passage on a specific reading platform | platform-bound passage | surface as `resolver_targets` entries with provenance so readers can jump from a stable reference to a useful platform | ## Where DOIs fit @@ -37,5 +108,6 @@ For the practical modelling distinction between external identifiers and reading - Treat TextRefs IDs as the **primary** identifier for a canonical reference. - Read external identifiers from a `Work`'s `alternateOf` and `isReferencedBy` arrays, and reading URLs from `CanonicalReference.resolver_targets`. These are enriched metadata, not the citation's identity. +- If you write annotations, target the TextRefs IRI when you mean the canonical passage. Keep your selectors, canvases, and TEI anchors for the representation you actually annotated. - If no mapping exists for the user's preferred edition, fall back to your own resolver chain. Link to a default mapping or to the TextRefs landing page. - When you publish your own data, attach a TextRefs ID alongside whatever you already issue. This is how the citation graph grows, and no one needs to change primary keys. From ac81fd43122ca4cf220a3a0ae17b6e96eb2a7b34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20M=C3=A4hr?= <14755525+maehr@users.noreply.github.com> Date: Thu, 13 Aug 2026 15:54:27 +0200 Subject: [PATCH 8/8] docs(standard): resolve review findings on the license and language rules (#79) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit State the published `license` form first in §9. The bullet led with the bare SPDX identifier, which is the authoring form, while every example in the section and §14 item 10 carry the canonical SPDX IRI. Say which grandfathered language tags `LanguageTag` rejects. The regex excludes all seventeen irregular tags, including the four without an `i-` prefix, and accepts the nine regular ones. A test locks the split. Co-Authored-By: Claude Opus 5 --- scripts/compile.test.ts | 45 ++++++++++++++++++++++ src/content/docs/standard/specification.md | 2 +- standard/schema/common.ts | 8 +++- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/scripts/compile.test.ts b/scripts/compile.test.ts index 6c09d5e..dc453d4 100644 --- a/scripts/compile.test.ts +++ b/scripts/compile.test.ts @@ -11,6 +11,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { v5 as uuidv5 } from 'uuid'; import { compileRegistry, type CompiledRegistry } from './compile.js'; +import { LanguageTag } from '../standard/schema/common.js'; type RegistryFiles = { systems: Record; @@ -591,3 +592,47 @@ test('a well-formed subtagged language tag is accepted', () => { 'grc-Grek', ); }); + +// RFC 5646 keeps grandfathered tags outside the langtag production it defines, +// so `LanguageTag` accepts or rejects them by whether they happen to parse as a +// langtag. This locks that split, which the comment on `LanguageTag` describes. +test('regular grandfathered tags parse and irregular ones do not', () => { + const regular = [ + 'art-lojban', + 'cel-gaulish', + 'no-bok', + 'no-nyn', + 'zh-guoyu', + 'zh-hakka', + 'zh-min', + 'zh-min-nan', + 'zh-xiang', + ]; + const irregular = [ + 'en-GB-oed', + 'i-ami', + 'i-bnn', + 'i-default', + 'i-enochian', + 'i-hak', + 'i-klingon', + 'i-lux', + 'i-mingo', + 'i-navajo', + 'i-pwn', + 'i-tao', + 'i-tay', + 'i-tsu', + 'sgn-BE-FR', + 'sgn-BE-NL', + 'sgn-CH-DE', + ]; + assert.deepEqual( + regular.filter((tag) => !LanguageTag.safeParse(tag).success), + [], + ); + assert.deepEqual( + irregular.filter((tag) => LanguageTag.safeParse(tag).success), + [], + ); +}); diff --git a/src/content/docs/standard/specification.md b/src/content/docs/standard/specification.md index f5374fe..a276b79 100644 --- a/src/content/docs/standard/specification.md +++ b/src/content/docs/standard/specification.md @@ -231,7 +231,7 @@ Required per entry: `url`, `access`. - `url` MUST be a dereferenceable external IRI ([RFC 3987](https://www.rfc-editor.org/rfc/rfc3987)). - `language` MUST be present when the entry is language-specific (e.g. a translation), as a [BCP 47](https://www.rfc-editor.org/info/bcp47) language tag ([RFC 5646](https://www.rfc-editor.org/rfc/rfc5646)). Tags MUST include an [ISO 15924](https://www.unicode.org/iso15924/) script subtag when the entry uses a non-default script for the language (e.g. `grc-Grek`, `hbo-Hebr`, `grc-Latn`). `edition` SHOULD name the specific edition or version when known. - `access` MUST be one of `open`, `paywalled`, `restricted`, `unknown`. -- `license`, when present, MUST be a current or deprecated [SPDX license identifier](https://spdx.org/licenses/) (e.g. `CC0-1.0`, `CC-BY-4.0`); published JSON-LD carries the canonical SPDX IRI (`https://spdx.org/licenses/{id}`), so `dcterms:license` has a single IRI-typed range. For licences not in the SPDX list, omit `license` and use the optional `license_url` to point at the licence text. +- `license`, when present, MUST be the canonical SPDX IRI (`https://spdx.org/licenses/{id}`) of a current or deprecated [SPDX license identifier](https://spdx.org/licenses/), so `dcterms:license` has a single IRI-typed range. Authoring formats carry the bare identifier (e.g. `CC0-1.0`, `CC-BY-4.0`) and the published record carries the IRI; see [§14](#14-validation-requirements) item 10. For licences not in the SPDX list, omit `license` and use the optional `license_url` to point at the licence text. - Values implying permission to host copyrighted full text (e.g. a `license` of `proprietary` accompanied by hosted text) are forbidden; the no-text rule in [§2](#2-conformance) governs. - A `CanonicalReference` whose `resolver_targets` is an empty array remains a valid identity record; adding or removing an entry MUST NOT change the parent reference's `id`. - Tombstoning a single bad URL is done by removing the entry; tombstoning the whole reference uses the parent `status` field. There is no independent status on individual entries. diff --git a/standard/schema/common.ts b/standard/schema/common.ts index a92c085..893fa92 100644 --- a/standard/schema/common.ts +++ b/standard/schema/common.ts @@ -32,8 +32,12 @@ export const FlatKey = z .regex(/^[a-z0-9][a-z0-9._-]*$/, 'flat key syntax: ^[a-z0-9][a-z0-9._-]*$'); // BCP 47 well-formedness (RFC 5646 langtag + privateuse). Registry validity is -// not checked — only that the tag parses. Irregular grandfathered tags -// (i-klingon, …) are out of scope: no text language in the registry needs one. +// not checked — only that the tag parses. Grandfathered tags split on that same +// line: the nine regular ones (art-lojban, zh-min-nan, …) are well-formed +// langtags and pass, and the seventeen irregular ones are out of scope — both +// the i- forms (i-klingon, …) and the four without that prefix (en-GB-oed, +// sgn-BE-FR, sgn-BE-NL, sgn-CH-DE). No text language in the registry needs one. +// `scripts/compile.test.ts` locks this split to the regex. export const LanguageTag = z .string() .regex(