Skip to content
4 changes: 2 additions & 2 deletions .github/ISSUE_TEMPLATE/conformance_failure.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
9 changes: 5 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<link rel="alternate">`.
- 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.

Expand Down
38 changes: 31 additions & 7 deletions api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ info:
- `/id/{type}/{key}.json` — JSON-LD for machines.

There is **no** `Accept`-header content negotiation. Clients either
follow the `<link rel="alternate" type="application/json" href="…json">`
follow the `<link rel="alternate" type="application/ld+json" href="…json">`
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
Expand Down Expand Up @@ -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'
Expand All @@ -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'
Expand All @@ -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:
Expand Down
68 changes: 68 additions & 0 deletions scripts/compile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>;
Expand Down Expand Up @@ -568,3 +569,70 @@ test('url_by with more than one selector variable is rejected at parse time', (t
);
assert.match(logged.join('\n'), /url_by takes exactly one selector variable/);
});

// --- Resolver language tags (BCP 47) -----------------------------------------

test('a malformed language tag is rejected at parse time', (t) => {
const message = expectCompileError(
t,
workWithResolver(` - url: 'https://example.org/{book}/{chapter}'
language: 'e n'`),
);
assert.match(message, /test\.work/);
});

test('a well-formed subtagged language tag is accepted', () => {
const reg = compileFixture(
workWithResolver(` - url: 'https://example.org/{book}/{chapter}'
language: grc-Grek`),
);
assert.equal(
reg.references.find((r) => r.locator === 'Gen.1')?.resolver_targets[0]
?.language,
'grc-Grek',
);
});

// RFC 5646 keeps grandfathered tags outside the langtag production it defines,
// so `LanguageTag` accepts or rejects them by whether they happen to parse as a
// langtag. This locks that split, which the comment on `LanguageTag` describes.
test('regular grandfathered tags parse and irregular ones do not', () => {
const regular = [
'art-lojban',
'cel-gaulish',
'no-bok',
'no-nyn',
'zh-guoyu',
'zh-hakka',
'zh-min',
'zh-min-nan',
'zh-xiang',
];
const irregular = [
'en-GB-oed',
'i-ami',
'i-bnn',
'i-default',
'i-enochian',
'i-hak',
'i-klingon',
'i-lux',
'i-mingo',
'i-navajo',
'i-pwn',
'i-tao',
'i-tay',
'i-tsu',
'sgn-BE-FR',
'sgn-BE-NL',
'sgn-CH-DE',
];
assert.deepEqual(
regular.filter((tag) => !LanguageTag.safeParse(tag).success),
[],
);
assert.deepEqual(
irregular.filter((tag) => LanguageTag.safeParse(tag).success),
[],
);
});
9 changes: 7 additions & 2 deletions scripts/source-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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
Expand Down
34 changes: 24 additions & 10 deletions src/content/docs/community/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Expand All @@ -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)):

Expand Down
Loading
Loading