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/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index fb0a100..61c1ea0 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -36,7 +36,7 @@ Re-minting changes a record's IRI. The old IRI must continue to resolve as a tombstone. See the [tombstones section in versioning.md](https://textrefs.org/standard/versioning/#tombstones-and-re-minted-records). - [ ] Old record retained with `status: withdrawn` -- [ ] A new `MappingAssertion` with `relation: exactMatch`, `subject: `, `target: ` links the old record to its successor (omit if there is no successor) +- [ ] The old record's `superseded_by` field (`dcterms:isReplacedBy`) links it to its successor's IRI (omit if there is no successor) - [ ] All other records that reference the old IRI have been audited (re-targeted to the new IRI, or themselves marked `withdrawn`) - [ ] Commit message uses `feat!:` / `fix!:` or `feat(scope)!:` / `fix(scope)!:` to signal the breaking IRI change diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f94564f..cf2d947 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,10 +1,40 @@ +# Version updates target `staging`, not the default branch. +# +# `main` only moves at release time, by squash-merging `staging`. A bump landed +# on `main` alone is therefore reverted by the next release: the squash applies +# staging's tree against a merge base that predates the bump. Everything that +# should survive a release has to reach `main` through `staging`. +# +# Note that `target-branch` governs version updates only — Dependabot security +# updates are always raised against the default branch, so advisory-driven PRs +# will still appear against `main` and still need folding into `staging` by +# hand. version: 2 updates: - package-ecosystem: github-actions directory: / + target-branch: staging schedule: interval: weekly groups: github-actions: patterns: - '*' + + - package-ecosystem: npm + directory: / + target-branch: staging + schedule: + interval: weekly + groups: + # Astro, Starlight and their plugins move in lockstep and are upgraded + # together or not at all; a lone bump here usually fails to resolve. + astro: + patterns: + - 'astro' + - '@astrojs/*' + - 'starlight*' + # First match wins, so this picks up everything else in one PR. + npm: + patterns: + - '*' diff --git a/.github/workflows/links.yml b/.github/workflows/links.yml index 5217e88..5cb72a6 100644 --- a/.github/workflows/links.yml +++ b/.github/workflows/links.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest if: github.repository == 'textrefs/textrefs.org' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: submodules: recursive @@ -28,7 +28,14 @@ jobs: - name: Collect external URLs from sources run: | mkdir -p lychee - grep -rhoE 'https?://[^"'\''<>) ]+' data/ src/content/docs/ \ + # Allow `)` inside URLs (Wikipedia disambiguation titles), then strip + # a single trailing `)` only when unbalanced (handles markdown + # `[text](url)`). Drop resolver templates containing `{placeholder}`. + # Restrict to YAML/MD so lockfiles inside data/ are not scanned. + grep -rhoE --include='*.yaml' --include='*.yml' --include='*.md' --include='*.mdx' \ + 'https?://[^]["'\''<>[:space:]]+' data/ src/content/docs/ \ + | awk '{ n=gsub(/\(/,"&"); m=gsub(/\)/,"&"); if (m>n) sub(/\)$/,""); print }' \ + | grep -v '[{}]' \ | sed -E 's/[.,;:]+$//' \ | sort -u > lychee/urls.txt echo "Collected $(wc -l < lychee/urls.txt) unique URLs" diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index fee5366..bc2ec8a 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -21,11 +21,11 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: submodules: recursive - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7 with: node-version: 24 cache: npm diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1335487..c22f383 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,11 +13,11 @@ jobs: publish-dump: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: submodules: recursive - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7 with: node-version: 24 cache: npm @@ -34,6 +34,7 @@ jobs: with: files: | dist/dump/*.jsonl + dist/dump/aliases.json dist/dump/datapackage.json fail_on_unmatched_files: true generate_release_notes: true diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 3264e44..c9256d0 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest if: github.repository == 'textrefs/textrefs.org' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: submodules: recursive - name: Verify data/ pin is on registry main @@ -25,7 +25,7 @@ jobs: echo "::error::data/ pointer $(git rev-parse HEAD) is not an ancestor of registry/main" exit 1 fi - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7 with: node-version: 24 cache: npm diff --git a/.zenodo.json b/.zenodo.json index e3da71a..b9f58dd 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -17,7 +17,8 @@ }, { "name": "Seiberth, Luz Christopher", - "affiliation": "TextRefs" + "affiliation": "TextRefs", + "orcid": "0000-0002-5606-0964" } ], "keywords": [ diff --git a/AGENTS.md b/AGENTS.md index bdbd39a..987f74d 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. @@ -24,12 +24,13 @@ Husky installs on `npm install`: Prettier runs on staged files, `commitlint` enf - `src/content/docs/{association,standard,api,community,get-started}/` — site sections (autogenerated sidebar) - `src/content/docs/de/` — German mirror (see Localization) - `src/pages/id/{work,system,ref,mapping}/` — canonical record pages (`/id/.../` HTML and `.json` JSON-LD siblings) -- `src/pages/reg/` — registry browser (index + paginated reference lists); does not host individual records -- `src/pages/cite/` — short-alias redirects (`/cite/{work}/{locator}` → canonical `/id/` URL) +- `src/pages/reg/` — registry browser (index + paginated reference lists), the registry-wide JSON-LD collections, and the per-work locator index (`/reg/work/{key}/aliases.json`); does not host individual records +- `src/pages/cite/` — short-alias redirects (`/cite/{work}/{system}/{locator}` always; bare `/cite/{work}/{locator}` for a work's preferred citation system, and retargetable if that preference changes) → canonical `/id/` URL - `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/CHANGELOG.md b/CHANGELOG.md index d1c34b7..01ca313 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,4 +2,51 @@ All notable changes to this project are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -Changelog tracking begins at `v0.1.0`. Pre-release history lives in the git log. +## [0.1.0] - 2026-08-12 + +### Added + +- Replace target_kind with dcterms:conformsTo (#6) +- Seed CanonicalReference UUIDs from the semantic identity tuple (ADR-0002) (#21) +- Draft lifecycle with retractable pre-promotion identity (ADR-0003) (#23) +- Require explicit canonical ASCII digit and case forms in profiles (#24) +- Flag draft records and exclude them from search indexing (#26) +- Upgrade to Astro 7 / Starlight 0.41, all dependencies to latest (#29) +- Minimal static templates for reference pages (#30) +- Collapse record lifecycle to draft → active (ADR-0004) (#62) +- Preferred citation system and qualified /cite/ aliases (ADR-0005) (#63) +- Replace SKOS mapping relations with alternateOf and isReferencedBy (ADR-0006) (#67) +- Map locator variables into a provider's own vocabulary (#71) (#72) + +### Documentation + +- Revise get-started prose and document staging deploy flow +- Sync statutes board size and review tracks with dossier +- Tighten dereferenceable-location guidance to should (#7) +- Self-contained §13 example with @context (#8) +- Add ORCID for Luz Christopher Seiberth (#19) + +### Fixed + +- Repair URL extraction in link-check workflow +- Mark docs/404.mdx as draft to drop catch-all route conflict +- Repair link-check workflow + bump deps to zod 4 (#5) +- Erratum batch and spec-consistency fixes (#10, #11, #12, #14) (#25) +- Resolve normative contradictions before the v0.1.0 tag (#44) +- Add explicit whitespace between inline elements (#53) (#54) +- List works instead of references on CitationSystem pages (#55) (#56) +- Release hardening before v0.1.0 (#46, #47, #49, #50) (#75) + +### Misc + +- Add textrefs/.github as submodule at github-profile/ +- Bump astro 6.4.4 and migrate to zod 4 +- Bump submodule with second resolvers on single-resolver works +- Bump submodule to registry main (36cae56) +- Bump submodule to registry main (40af385) +- Astro 6.4.8, dompurify 3.4.11, actions/checkout v7 +- Bump submodule to registry main (c0a3275, ORCID docs) +- Astro 7.1 + dependency refresh (#39) (#43) +- Bump registry pin to the ADR-0006 reclassification +- Bump registry pin to the resolver review +- Refresh all dependencies before v0.1.0 (#73) diff --git a/CITATION.cff b/CITATION.cff index 55cc761..b56d6e8 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,6 +1,8 @@ cff-version: 1.2.0 message: 'If you cite TextRefs, please cite it as below.' title: TextRefs +version: 0.1.0 +date-released: '2026-08-12' abstract: An open, persistent, machine-readable registry for canonical text references. type: software authors: @@ -13,8 +15,8 @@ authors: - family-names: Seiberth given-names: Luz Christopher email: luz@seiberth.de + orcid: 'https://orcid.org/0000-0002-5606-0964' affiliation: TextRefs - # ORCID: TBD repository-code: 'https://github.com/textrefs/textrefs.org' url: 'https://textrefs.org' license: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6d941c1..3d5dfd1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,22 +26,21 @@ A contribution does not create a claim to acceptance, prioritization, publicatio ## Review tracks -Changes are routed to one of two tracks: +Changes are routed to one of three tracks: -- **Technical review** — typos, formatting, broken links, minor metadata, `last_checked` updates, uncontested aliases, build / tooling fixes. Needs automated validation and one technical reviewer. -- **Expert review** — new works, new citation systems, new corpora, contested mappings, changes to deterministic ID inputs, status changes (`active` / `deprecated` / `withdrawn` / `blocked`). Needs technical validation, a documented rationale with sources, and at least one expert reviewer. - -The Board reserves decisions on legal or policy-sensitive matters (takedowns, blocking, licence policy). +- **Technical review** — typos, formatting, broken links, minor metadata, `last_checked` updates, uncontested aliases, build / tooling fixes, and merging new registry data as `draft`. Needs automated validation and one technical reviewer. +- **Expert review** — new works, new citation systems, new corpora, contested mappings, changes to deterministic ID inputs, status changes (promotion `draft` → `active`, and `deprecated` / `withdrawn` / `blocked`). Needs technical validation, a documented rationale with sources, and at least one expert reviewer. +- **Board reservation** — takedowns, blocking, licence policy, and other legal or policy-sensitive matters. Decided by the Association Board. ```mermaid flowchart TD S["Contribution
(issue or PR)"] --> T{Triage} T -->|"typos, formatting, links, metadata, tooling"| TR[Technical review] - T -->|"new work / system / corpus, contested mapping, ID inputs, status change"| ER[Expert review] + T -->|"new work / system / corpus, contested mapping, ID inputs, status change / promotion"| ER[Expert review] T -->|"takedown, blocking, licence / policy"| BR[Board reservation] TR --> V{"Automated validation
+ 1 technical reviewer"} ER --> V2{"Validation + rationale and sources
+ 1 expert reviewer"} - V -->|pass| A([Accepted / merged]) + V -->|pass| A(["Accepted / merged
(new data lands as draft)"]) V -->|fail| R([Rejected, with reason]) V2 -->|pass| A V2 -->|fail| R @@ -49,6 +48,8 @@ flowchart TD BR -->|decision| R ``` +New registry records enter at `status: draft` after technical review; they stay retractable until an expert review promotes them to `active`, which permanently freezes their identifier (see the [versioning rules](https://textrefs.org/standard/versioning/) and governance §5). + ## Local development Prerequisites: Node 24 and npm. @@ -65,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. @@ -91,13 +92,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 @@ -109,13 +122,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/README.md b/README.md index 91e12c8..143680a 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 @@ -68,19 +71,19 @@ Prerequisites: Node 24 and npm. Configuration lives in `.env`; use [`.env.example`](./.env.example) as the starting point. `SITE_DOMAIN` controls Astro's canonical `site` URL and defaults to `textrefs.org` when unset. -| Command | Action | -| :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `npm install` | Install dependencies; wires git hooks (husky + lint-staged) | -| `npm run dev` | Start local dev server at `localhost:4321` | -| `npm run build` | Build the production site to `./dist/` | -| `npm run build:fast` | Build the site against a tiny fixture registry, without compiling full data | -| `npm run preview` | Preview the build locally | -| `npm run compile:data` | Read hand-authored YAML under `data/works/` and `data/systems/`, expand the in-memory registry, and emit JSONL resources plus `datapackage.json` under `dist/dump/` | -| `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 changelog` | Regenerate `CHANGELOG.md` from git history (git-cliff) | +| Command | Action | +| :---------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `npm install` | Install dependencies; wires git hooks (husky + lint-staged) | +| `npm run dev` | Start local dev server at `localhost:4321` | +| `npm run build` | Build the production site to `./dist/` | +| `npm run build:fast` | Build the site against a tiny fixture registry, without compiling full data | +| `npm run preview` | Preview the build locally | +| `npm run compile:data` | Read hand-authored YAML under `data/works/` and `data/systems/`, expand the in-memory registry, and emit JSONL resources, the alias table, and `datapackage.json` under `dist/dump/` | +| `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` + 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. @@ -111,13 +114,13 @@ Both deposits live in the [TextRefs Zenodo community](https://zenodo.org/communi | 🎁 Feature requests | [GitHub Issues](https://github.com/textrefs/textrefs.org/issues) | | 📊 Bad data / mapping corrections | [GitHub Issues](https://github.com/textrefs/textrefs.org/issues) (label: `data`) | | 📚 Docs issues | [GitHub Issues](https://github.com/textrefs/textrefs.org/issues) | -| 🛡 Security vulnerabilities | See [`SECURITY.md`](./SECURITY.md) — private GitHub advisory | +| 🛡 Security vulnerabilities | See [`SECURITY.md`](./SECURITY.md) — private GitHub advisory | | 🤝 Code-of-Conduct concerns | | | 💬 General questions | [GitHub Discussions](https://github.com/textrefs/textrefs.org/discussions) | ## Roadmap -TextRefs is **pre-1.0**: the association is being founded, the standard is being drafted, and the current registry examples are candidate data. Public milestones will appear on the [GitHub project board](https://github.com/textrefs/textrefs.org/projects) once it is set up. The statutes ([English](https://textrefs.org/association/statutes/), [Deutsch](https://textrefs.org/de/association/statutes/)) and governance regulation describe the long-term scope. +TextRefs is **pre-1.0**: the association is being founded, the standard is being drafted, and the current registry examples are draft data. Public milestones will appear on the [GitHub project board](https://github.com/textrefs/textrefs.org/projects) once it is set up. The statutes ([English](https://textrefs.org/association/statutes/), [Deutsch](https://textrefs.org/de/association/statutes/)) and governance regulation describe the long-term scope. ## Contributing @@ -132,7 +135,7 @@ Two release trains live in two repositories: - **TextRefs Standard** (this repo) — tags `vMAJOR.MINOR.PATCH[-prerelease]` advance the spec, schemas, and site together. The spec's maturity level (`working-draft` → `candidate-recommendation` → `recommendation`) is encoded in each `/standard/*` page's frontmatter; the SemVer tag encodes pre-release status. - **TextRefs Registry** ([`textrefs/registry`](https://github.com/textrefs/registry)) — calendar tags `vYYYY.MM.N` cut monthly registry exports. The data-package `version` inside `datapackage.json` follows SemVer-without-`v`. -Records can be re-minted (e.g. when a `work` key is renamed). The old IRI continues to resolve as a tombstone (`status: withdrawn`); successors are linked by an `exactMatch` `MappingAssertion`. See [versioning policy](https://textrefs.org/standard/versioning/) for the full rules. +Records can be re-minted (e.g. when a `work` key is renamed). The old IRI continues to resolve as a tombstone (`status: withdrawn`); successors are linked by the `superseded_by` field (`dcterms:isReplacedBy`). See [versioning policy](https://textrefs.org/standard/versioning/) for the full rules. ## Contributors and roles diff --git a/ROADMAP.md b/ROADMAP.md index 8fa0b67..8c70240 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,7 +4,7 @@ This is the public roadmap. The same content is mirrored on the docs site at `. - 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. ## Next -- planned: Publish the Standard draft v0.1.0 as the first citable baseline. +- done: Publish the Standard draft v0.1.0 as the first citable baseline (release `v0.1.0`). - planned: Serve the generated JSON Schema at a stable `/schemas/` path or update the specification if the publication path changes. - planned: Add `Accept-Language` / `edition`-based 303 redirect on `/id/ref/{uuid}` to a matching `resolver_targets` entry. - planned: Publish regular registry exports from the compiled data bundle. diff --git a/api/openapi.yaml b/api/openapi.yaml index ffc9d79..ee93adb 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 @@ -21,6 +21,22 @@ info: The `Accept-Language` / `edition`-based 303 redirect on `/id/ref/{uuid}` to a resolver-target manifestation is planned but not yet live. + + A client that does not yet know a key starts at `/reg/works.json` or + `/reg/systems.json`. A client that knows a work key, a citation system + key, and a locator reads the reference UUID from + `/reg/work/{key}/aliases.json`. It needs no UUIDv5 implementation of its + own. `/dump/` carries the whole registry as newline-delimited JSON, plus + the whole alias table as one JSON object, for bulk consumers. + + The site is static, and the host derives every `Content-Type` from the + file extension. This contract therefore declares the media type that + the host actually sends, not the media type that best describes the + payload. A record or collection `.json` body is JSON-LD by content and + arrives as `application/json`. The two alias artifacts are plain JSON + and arrive the same way. A `.jsonl` body is newline-delimited JSON by + content and arrives as `application/octet-stream`. Parse by documented + shape, not by response header. license: name: AGPL-3.0-or-later url: https://www.gnu.org/licenses/agpl-3.0.html @@ -33,6 +49,17 @@ servers: tags: - name: Records description: Canonical record URLs under `/id/`. + - name: Collections + description: + Registry-wide JSON-LD collections and per-work locator indexes under + `/reg/`. Entry points for a client that knows no key, or that knows a + locator but no reference UUID. + - name: Bulk + description: + Whole-registry artifacts under `/dump/`, rebuilt on every deploy. The + `.jsonl` bodies are newline-delimited JSON, and the host serves them + as `application/octet-stream`. The `.json` bodies are single JSON + objects, and the host serves them as `application/json`. paths: /id/work/{key}/: get: @@ -146,6 +173,145 @@ paths: application/json: schema: { $ref: '#/components/schemas/MappingAssertion' } '404': { $ref: '#/components/responses/NotFound' } + /reg/works.json: + get: + tags: [Collections] + summary: Work collection — JSON-LD + operationId: getWorkCollection + description: + Each item in `@graph` is the same record that `/id/work/{key}.json` + serves, without its own `@context`. The collection carries one + `@context` for the whole graph. + responses: + '200': + description: + Every Work record in the registry, in every status, sorted by + key. + content: + application/json: + schema: { $ref: '#/components/schemas/WorkCollection' } + /reg/systems.json: + get: + tags: [Collections] + summary: CitationSystem collection — JSON-LD + operationId: getCitationSystemCollection + description: Each item in `@graph` is the same record that + `/id/system/{key}.json` serves, without its own `@context`. The + collection carries one `@context` for the whole graph. + responses: + '200': + description: + Every CitationSystem record in the registry, in every status, + sorted by key. + content: + application/json: + schema: { $ref: '#/components/schemas/SystemCollection' } + /reg/work/{key}/aliases.json: + get: + tags: [Collections] + summary: Work locator index — JSON + operationId: getWorkAliasIndex + description: Maps every locator of one work to the UUID of its + CanonicalReference, grouped by citation system key. A client that + knows a passage as work, citation system, and locator reads the UUID + here, then fetches `/id/ref/{uuid}.json`. It needs no UUIDv5 + implementation of its own. The body is plain JSON, not JSON-LD, and + carries no `@context`. + parameters: + - $ref: '#/components/parameters/FlatKey' + responses: + '200': + description: + Every reference of this work, in every status, keyed by citation + system key and then by locator. + content: + application/json: + schema: { $ref: '#/components/schemas/WorkAliasIndex' } + '404': { $ref: '#/components/responses/NotFound' } + /dump/works.jsonl: + get: + tags: [Bulk] + summary: Work dump — NDJSON + operationId: getWorksDump + responses: + '200': + description: + Newline-delimited JSON, one Work record object per line, with + no `@context`. + content: + application/octet-stream: + schema: { type: string } + /dump/citation-systems.jsonl: + get: + tags: [Bulk] + summary: CitationSystem dump — NDJSON + operationId: getCitationSystemsDump + responses: + '200': + description: + Newline-delimited JSON, one CitationSystem record object per + line, with no `@context`. + content: + application/octet-stream: + schema: { type: string } + /dump/references.jsonl: + get: + tags: [Bulk] + summary: CanonicalReference dump — NDJSON + operationId: getReferencesDump + responses: + '200': + description: + Newline-delimited JSON, one CanonicalReference record object + per line, with no `@context`. + content: + application/octet-stream: + schema: { type: string } + /dump/mappings.jsonl: + get: + tags: [Bulk] + summary: MappingAssertion dump — NDJSON + operationId: getMappingsDump + responses: + '200': + description: + Newline-delimited JSON, one MappingAssertion record object per + line, with no `@context`. + content: + application/octet-stream: + schema: { type: string } + /dump/aliases.json: + get: + tags: [Bulk] + summary: Alias table — JSON + operationId: getAliasesDump + description: + The complete alias table as one JSON object, with sorted keys and no + indentation. For a single lookup, use + `/reg/work/{key}/aliases.json` instead. This file serves a consumer + that mirrors the whole table. + responses: + '200': + description: One JSON object. Each key is an alias. Each value is the + canonical IRI that the alias resolves to. + content: + application/json: + schema: { $ref: '#/components/schemas/AliasTable' } + /dump/datapackage.json: + get: + tags: [Bulk] + summary: Frictionless data-package descriptor + operationId: getDataPackage + responses: + '200': + description: + The Frictionless data-package descriptor that lists the five + resources — the four JSONL files and the JSON alias table — + with, for each, its byte count and its `sha256:` hash, plus the + registry version. + content: + application/json: + schema: { type: object } components: parameters: FlatKey: @@ -180,12 +346,9 @@ components: IsoDate: type: string pattern: '^\d{4}-\d{2}-\d{2}$' - SemVer: - type: string - pattern: '^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$' Status: type: string - enum: [candidate, active, deprecated, withdrawn, blocked] + enum: [draft, active, deprecated, withdrawn, blocked] AdminMetadata: type: object required: [status, created, modified] @@ -193,11 +356,36 @@ components: status: { $ref: '#/components/schemas/Status' } created: { $ref: '#/components/schemas/IsoDate' } modified: { $ref: '#/components/schemas/IsoDate' } + superseded_by: + allOf: + - $ref: '#/components/schemas/Iri' + 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' - type: object - required: [id, key, type, preferred_label] + required: + [id, key, type, preferred_label, preferred_citation_system_key] properties: id: type: string @@ -205,20 +393,48 @@ components: key: { $ref: '#/components/schemas/FlatKey' } type: { type: string, enum: [Work] } preferred_label: { type: string, minLength: 1 } + alternative_labels: + type: array + minItems: 1 + uniqueItems: true + items: { type: string, minLength: 1 } + description: + Additional names for the work — abbreviations, translated + titles, and established short forms. Search and display only. + Identity-neutral, because no label is a UUID seed input, so a + label change never moves an identifier. Unique within one work; + two different works MAY share an alternative label. + preferred_citation_system_key: + allOf: + - $ref: '#/components/schemas/FlatKey' + description: + Key of the CitationSystem this work is cited under by default. + 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 alternateOf MappingAssertions + that are not deprecated, withdrawn or blocked. The array enriches + the work and makes no review claim. Read the MappingAssertion for + status. Read-only, never authored. + isReferencedBy: + type: array + items: { $ref: '#/components/schemas/Iri' } + description: Compiler-derived from the work's isReferencedBy + MappingAssertions that are not deprecated, withdrawn or blocked. + The array enriches the work and makes no review claim. Read the + MappingAssertion for status. Read-only, never authored. CitationSystem: allOf: - $ref: '#/components/schemas/AdminMetadata' - type: object - required: - [ - id, - key, - type, - preferred_label, - normalization_version, - locator_regex, - examples, - ] + required: [id, key, type, preferred_label, description, locator_regex] properties: id: type: string @@ -226,26 +442,17 @@ components: key: { $ref: '#/components/schemas/FlatKey' } type: { type: string, enum: [CitationSystem] } preferred_label: { type: string, minLength: 1 } - normalization_version: { $ref: '#/components/schemas/SemVer' } + description: { type: string, minLength: 1 } locator_regex: { type: string, minLength: 1 } - examples: - type: object - required: [valid, invalid] - properties: - valid: - type: array - items: { type: string } - minItems: 1 - invalid: - type: array - items: { type: string } - minItems: 1 ResolverTargetEntry: type: object 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: @@ -262,15 +469,7 @@ components: - $ref: '#/components/schemas/AdminMetadata' - type: object required: - [ - id, - type, - work_key, - citation_system_key, - locator, - normalization_version, - resolver_targets, - ] + [id, type, work_key, citation_system_key, locator, resolver_targets] properties: id: type: string @@ -279,7 +478,6 @@ components: work_key: { $ref: '#/components/schemas/FlatKey' } citation_system_key: { $ref: '#/components/schemas/FlatKey' } locator: { type: string, minLength: 1 } - normalization_version: { $ref: '#/components/schemas/SemVer' } resolver_targets: type: array default: [] @@ -300,14 +498,86 @@ components: description: MUST be a Work IRI. relation: type: string - enum: [exactMatch, closeMatch] + enum: [alternateOf, isReferencedBy] + description: Chosen by what the target is (ADR-0006). alternateOf + (prov:alternateOf) — another entity denoting the same work. + isReferencedBy (dcterms:isReferencedBy) — a document or page + about the work. target: type: object required: [identifier] properties: - target_kind: { type: string } identifier: { $ref: '#/components/schemas/Iri' } + conforms_to: + description: + Informative. IRI (or array of IRIs) of the specification the + target conforms to. Validators MUST NOT key behaviour off it. + oneOf: + - $ref: '#/components/schemas/Iri' + - type: array + items: { $ref: '#/components/schemas/Iri' } + minItems: 1 source: { type: string, minLength: 1 } + WorkCollection: + description: The `/reg/works.json` registry-wide collection of Work records. + type: object + required: ['@context', '@graph'] + properties: + '@context': { type: string, format: uri } + '@graph': + type: array + items: { $ref: '#/components/schemas/Work' } + SystemCollection: + description: + The `/reg/systems.json` registry-wide collection of CitationSystem + records. + type: object + required: ['@context', '@graph'] + properties: + '@context': { type: string, format: uri } + '@graph': + type: array + items: { $ref: '#/components/schemas/CitationSystem' } + WorkAliasIndex: + description: + The `/reg/work/{key}/aliases.json` locator index of one work. Plain + JSON, not JSON-LD; it carries no `@context`. The index states + identity alone. Read the CanonicalReference for the status and the + resolver targets. + type: object + required: [work_key, preferred_citation_system_key, refs] + properties: + work_key: { $ref: '#/components/schemas/FlatKey' } + preferred_citation_system_key: + allOf: + - $ref: '#/components/schemas/FlatKey' + description: + The system that the bare `/cite/{work_key}/{locator}` alias uses + (ADR-0005). A default for presentation, never an identity claim. + The other keys of `refs` have equal standing. + refs: + type: object + description: One entry per citation system key that this work carries + references under. An empty object means the work has no + references yet. The same locator under two systems denotes a + different passage, and a different UUID, each time. + additionalProperties: + type: object + description: Locator to CanonicalReference UUID. + additionalProperties: + type: string + pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' + AliasTable: + description: + The `/dump/aliases.json` table, with one entry per alias. A key is + either a `/cite/` alias path — `{work_key}/{locator}` or + `{work_key}/{citation_system_key}/{locator}` — or an external + identifier IRI carried by a MappingAssertion. A value is the + canonical IRI that the key resolves to — an `/id/ref/` IRI for a + `/cite/` alias, an `/id/work/` IRI for an external identifier. A + `://` in the key marks the second kind. + type: object + additionalProperties: { $ref: '#/components/schemas/Iri' } RegistryObject: oneOf: - { $ref: '#/components/schemas/Work' } diff --git a/astro.config.mjs b/astro.config.mjs index f3e085a..caa319b 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -6,16 +6,24 @@ import starlightLinksValidator from 'starlight-links-validator'; import starlightLlmsTxt from 'starlight-llms-txt'; import starlightOpenAPI, { openAPISidebarGroups } from 'starlight-openapi'; import starlightBlog from 'starlight-blog'; +import sitemap from '@astrojs/sitemap'; +import { buildNoindexPredicate } from './src/lib/noindex.ts'; const siteDomain = process.env.SITE_DOMAIN ?? 'textrefs.org'; const site = siteDomain.startsWith('http') ? siteDomain : `https://${siteDomain}`; +const isNoindex = buildNoindexPredicate(); + export default defineConfig({ site, integrations: [ mermaid({ autoTheme: true }), + // Declared explicitly so the sitemap can drop draft record pages; + // Starlight adds `@astrojs/sitemap` with default options only when the + // project has not already registered it. + sitemap({ filter: (page) => !isNoindex(new URL(page).pathname) }), starlight({ plugins: [ starlightBlog({ @@ -32,7 +40,7 @@ export default defineConfig({ }), starlightLinksValidator({ errorOnFallbackPages: false, - exclude: ['/id/**', '/reg/**', '/cite/**', '/api/**'], + exclude: ['/id/**', '/reg/**', '/cite/**', '/api/**', '/find/**'], }), starlightOpenAPI([ { @@ -95,7 +103,10 @@ export default defineConfig({ { label: 'Registry', translations: { de: 'Verzeichnis' }, - items: [{ label: 'Browse', link: '/reg/' }], + items: [ + { label: 'Find a reference', link: '/find/' }, + { label: 'Browse', link: '/reg/' }, + ], }, ...openAPISidebarGroups, { diff --git a/data b/data index 6d04b70..237e39f 160000 --- a/data +++ b/data @@ -1 +1 @@ -Subproject commit 6d04b70c61fce0c3402ea2f8eac7a5a9ea886e79 +Subproject commit 237e39f4767e9e65ffe4bee77c10f82d490cd223 diff --git a/decisions/ADR-0001-conforms-to-replaces-target-kind.md b/decisions/ADR-0001-conforms-to-replaces-target-kind.md new file mode 100644 index 0000000..2dfe66a --- /dev/null +++ b/decisions/ADR-0001-conforms-to-replaces-target-kind.md @@ -0,0 +1,57 @@ +# ADR-0001: Replace `target_kind` with `dcterms:conformsTo` + +- **Status:** Accepted +- **Date:** 2026-06-09 +- **Deciders:** @maehr +- **Tags:** spec + +## Context and problem statement + +`MappingAssertion.target.target_kind` was introduced as an OPTIONAL human-readable scheme hint (`"cts"`, `"wikidata"`, `"doi"`, …). The spec already says validators MUST NOT key behaviour off it and the IRI in `identifier` is authoritative. Issue [#6](https://github.com/textrefs/textrefs.org/issues/6) observes that a field with no normative weight, accompanied by Appendix B's enumerated list of "known" scheme labels, is upkeep without payoff: every new scheme means another table row to police. + +Linked Art's [digital integration model](https://linked.art/model/digital/) handles the same problem with `conforms_to` pointing at the relevant specification (e.g. the IIIF profile URI), letting the IRI carry the conformance claim without a curated label registry. + +## Decision drivers + +- The field is already non-normative; the IRI is authoritative. +- Appendix B's label column would otherwise grow indefinitely. +- Pre-v1.0.0 — breaking field renames are still acceptable. +- Want to align with established Linked Data practice rather than invent a TextRefs-specific convention. + +## Considered options + +1. **Replace `target_kind` with `conforms_to`** — drop the field, introduce an optional `target.conforms_to` typed as `dcterms:conformsTo` in the JSON-LD context, accepting an IRI or array of IRIs. +2. **Deprecate `target_kind`, keep accepting it** — add `conforms_to` alongside, mark `target_kind` deprecated. Smoother for downstream consumers; carries the dead field into v1. +3. **Leave `target_kind` as-is** — no change. Fails to address the upkeep concern that prompted the issue. + +## Decision + +We choose **Option 1**. The replacement happens before v1.0.0 freezes the schema, so a clean break is preferable to carrying a deprecated field into the stable surface. `target.conforms_to` is OPTIONAL; the registry will not synthesise values where none are known. Multiple IRIs are allowed via array form so a target can claim conformance to several specifications. + +In the JSON-LD context, `conforms_to` maps to `dcterms:conformsTo` with `@type: @id` so the value is treated as an IRI reference. + +## Consequences + +### Positive + +- One less curated label registry to maintain. +- IRIs are self-describing; conformance claims are dereferenceable. +- Aligns with Linked Art's established pattern. + +### Negative / trade-offs + +- Breaking rename for any downstream consumer that read `target_kind`. Acceptable pre-v1.0.0. +- Coordinated change across two repos (`textrefs/textrefs.org` schema/docs, `textrefs/registry` YAML data). + +### Follow-up actions + +- [x] Update spec §10 and Appendix B. +- [x] Update the v1 JSON-LD context. +- [x] Update Zod schema, compile pipeline, and in-tree fixture. +- [x] Migrate `data/works/*.yaml` in `textrefs/registry` (carried by registry PR #1). +- [x] Bump `data/` submodule pointer to the migrated registry commit. + +## Links + +- Related issues / PRs: textrefs/textrefs.org#6, textrefs/textrefs.org#5, textrefs/registry#1 +- External references: [Linked Art — Digital Integration](https://linked.art/model/digital/), [DCMI Terms — conformsTo](http://purl.org/dc/terms/conformsTo) diff --git a/decisions/ADR-0002-uuid-seed-semantic-identity.md b/decisions/ADR-0002-uuid-seed-semantic-identity.md new file mode 100644 index 0000000..602e61c --- /dev/null +++ b/decisions/ADR-0002-uuid-seed-semantic-identity.md @@ -0,0 +1,74 @@ +# ADR-0002: Seed CanonicalReference UUIDs from the semantic identity tuple + +- **Status:** Accepted +- **Date:** 2026-07-05 +- **Deciders:** @maehr +- **Tags:** spec + +## Context and problem statement + +The `CanonicalReference` UUID v5 seed included four fields: `work_key`, `citation_system_key`, `locator`, and `normalization_version`, with `normalization_version` fixed at minting time. Issue [#15](https://github.com/textrefs/textrefs.org/issues/15) observes the consequence: a third party holding only `(work_key, citation_system_key, locator)` cannot compute the registry UUID, because they cannot know which `normalization_version` was in force when that reference was minted. They must look the reference up first — at which point they already have the IRI. "Deterministic" therefore did not mean "offline-computable from semantic fields", and the spec was silent about which purpose determinism actually serves. + +TextRefs has not yet published authoritative reference data. UUIDs are computed at compile time from enumerated YAML source; none are stored in the registry. Changing the seed now recomputes identifiers at zero migration cost; changing it after publication would be a registry-wide tombstone event. + +## Decision drivers + +- The intuitive public contract for a registry of canonical references: same work + same citation system + same canonical locator = same reference ID. +- Offline computability for third-party tooling that holds the canonical fields. +- Pre-v1.0.0 and pre-data: a breaking seed change is still nearly free (precedent: ADR-0001). +- An identity field whose contract needs a lookup to explain (issue #15) breeds the next round of confusion; a version field kept "just as metadata" would do the same. +- Multiple regex-valid spellings of one passage would mint permanent identity splits (issue [#13](https://github.com/textrefs/textrefs.org/issues/13)), so removing the version discriminator must be paired with strict canonical locator forms. + +## Considered options + +1. **Keep the 4-field seed; declare determinism's purpose as compiler/mirror reproducibility** — zero migration; documents that IDs are not offline-computable. (Issue #15's original recommendation.) +2. **Remove `normalization_version` from the seed, keep loose normalization** — offline-computable IDs, but silently re-opens the `John.3.16` / `john.3.16` identity-split class. +3. **Keep the seed; tell clients to compute with the current version and fall back to lookup** — false sense of offline computability; silent mis-mints. +4. **Revised option 2: 3-field seed + strict canonical locator forms + drop `normalization_version` from the data model** — offline-computable IDs with the split window closed by profile strictness instead of a version discriminator. + +## Decision + +We choose **Option 4**. Because TextRefs has not yet published authoritative reference data, canonical-reference UUIDs become computable from the semantic identity tuple: `work_key`, `citation_system_key`, and the canonical `locator`, LF-joined in that order (namespace UUID unchanged). + +`normalization_version` is removed from the data model entirely — not demoted to metadata. Once outside the seed it would version nothing that matters: a normalization change that alters any accepted locator is compatibility-sensitive by the rules below regardless of any version label, and rule evolution that does not alter locators is visible through `modified`. Keeping a vestigial version field would reproduce the confusion that motivated issue #15. + +The accompanying rules that make the 3-field seed safe: + +- **Citation-system profiles define canonical forms.** Each profile defines exactly one canonical spelling per reference point, in `locator_regex` (machine-checkable constraints, including ASCII digit and case forms — issue #13) and in the new required `description` field (prose constraints). +- **Non-canonical locators are rejected at validation time**, never silently folded into the canonical form. The compiler's regex gate stays a hard error. +- **The enumerated registry data is the canon.** A locator is canonical and attested because a curated work record enumerates it; `locator_regex` is the machine-checkable floor beneath curation, not the definition of validity. +- **Canonicalization changes are compatibility events.** A profile change that would alter the canonical spelling of any accepted locator is a pre-1.0 registry migration, a breaking registry release, or — when the distinction is genuinely semantic — a new `citation_system_key`. Never a silent change. + +Two simplifications ride along, keeping the `CitationSystem` shape honest after the field removal: + +- The `examples.valid` / `examples.invalid` block is removed. Its only job was self-testing `locator_regex`; the compiler already tests the regex against every enumerated locator of every work, a strictly larger test set. +- The explanatory YAML comments in system files move into the required `description` field (`dcterms:description` in the JSON-LD context), so the prose part of the profile contract is published rather than buried in source comments. + +## Consequences + +### Positive + +- Same work + same citation system + same canonical locator = same reference ID, computable offline. +- No identity-bearing field whose value depends on registry state at minting time. +- Profile discipline (canonical forms, rejection over folding) is forced now, before data exists, instead of compensated for later. +- Leaner `CitationSystem` records: one label, one description, one regex. + +### Negative / trade-offs + +- Every compiled reference UUID changes. Acceptable now (nothing published, nothing stored); it would not be later. +- Without a version discriminator, locator-affecting profile changes have no soft path — by design, but it makes profile authors' first regex more consequential. +- The regexes originally shipped were laxer than the canonical-form rules demand (Bekker leading zeros, Bible case variants); #13 closed that gap alongside this ADR, and profiles now declare leading-zero policy and canonical case explicitly. Curation remains the only guard for constraints no regex can express. + +### Follow-up actions + +- [x] Update spec (`specification.md` §§5, 7, 8, 11, 13, 14; `identifier-syntax.md`; `system-profiles.md`; `json-ld.md`) and get-started docs. +- [x] Update the v1 JSON-LD context (drop `normalization_version`, `examples`, `valid`, `invalid`; add `description`). +- [x] Update Zod schemas, compile/validate pipeline, in-tree fixture, and record pages. +- [x] Migrate `data/systems/*.yaml` in `textrefs/registry` and bump the `data/` submodule pointer. +- [x] Tighten per-profile `locator_regex` canonical digit/case forms under issue #13 (textrefs/registry#2, textrefs/registry#3). + +## Links + +- Related ADRs: ADR-0001 (precedent for a pre-v1.0.0 breaking change) +- Related issues / PRs: textrefs/textrefs.org#15, textrefs/textrefs.org#13, textrefs/textrefs.org#9, textrefs/registry#2, textrefs/registry#3 +- External references: [RFC 9562 — UUID v5](https://www.rfc-editor.org/rfc/rfc9562) diff --git a/decisions/ADR-0003-draft-lifecycle-ephemeral-identity.md b/decisions/ADR-0003-draft-lifecycle-ephemeral-identity.md new file mode 100644 index 0000000..6c2f7f0 --- /dev/null +++ b/decisions/ADR-0003-draft-lifecycle-ephemeral-identity.md @@ -0,0 +1,73 @@ +# ADR-0003: Review lifecycle with an ephemeral `draft` state + +- **Status:** Superseded by ADR-0004 +- **Date:** 2026-07-05 +- **Deciders:** @maehr +- **Tags:** spec, governance + +## Context and problem statement + +Every published record carries the full persistence promise: IRIs are permanent once minted (identifier-syntax § Immutability, specification §11), and any identity correction requires tombstone ceremony (versioning § Tombstones). There is no state in which a record is visible in the registry but still retractable — a wrong locator cannot simply be fixed, a bad entry cannot simply vanish. Issue [#22](https://github.com/textrefs/textrefs.org/issues/22) proposes such a state. + +The governance regulation defines review _tracks_ (§4: technical / expert / board) but no in-registry review _lifecycle_: nothing distinguishes freshly imported, unreviewed data from records that have passed expert review. + +Since ADR-0002, identity is pure math: the reference UUID is offline-computable from `(work_key, citation_system_key, locator)`. That separates **identity** (which never mutates — a tuple always hashes to the same UUID) from **registration** (what the registry endorses and promises). It also puts governance §6.4 ("provisional or uncertain objects receive generated IDs") in conflict with the deterministic-identity model. + +## Decision drivers + +- Reviewers need to browse rendered records and check resolver targets _before_ the registry commits to permanence. +- Corrections during review should be cheap: fix the tuple, the wrong UUID disappears — no tombstone debt for data nobody ever relied on. +- The persistence promise must stay absolute where it applies; a promise with fuzzy edges is worse than a narrower hard one. +- ADR-0002's offline computability must hold at every lifecycle stage; re-minting ceremonies contradict it. +- The DE governance regulation is the legally binding text; the model must be expressible there in sync. + +## Considered options + +1. **Attach the promise at `candidate → active`** — no new status; `candidate` becomes retractable. Silently weakens what `candidate` means today and leaves no distinction between "just imported" and "proposed for acceptance". +2. **Never publish ephemeral records (PR-only review)** — everything published stays permanent. Reviewers cannot browse rendered records; long-running review branches become the de-facto ephemeral state. +3. **v4 provisional IDs, re-minted to v5 at promotion** (governance §6.4 as written) — ephemerality visible in the ID itself, but breaks offline computability for drafts and makes every promotion an ID migration. +4. **New `draft` status; promise attaches at `draft → candidate` promotion; deterministic v5 throughout.** + +## Decision + +We choose **Option 4**. The status ladder becomes `draft` → `candidate` → `active` → `deprecated` / `withdrawn` / `blocked`, and the identifier-persistence promise attaches at the **promotion** event (`draft → candidate`), not at publication. + +**Entry.** New data enters the registry as `status: draft` after technical review (governance §4.1: schema validation, `locator_regex`, no full text). Draft records are rendered with a prominent "draft — not a persistent identifier" treatment (`noindex`, excluded from site search) and appear in exports with `status` as the signal — the same convention tombstones use. + +**Ephemerality.** Draft records are excluded from the persistence policy. They MAY be corrected — changing an identity field mints a different id; the old UUID simply disappears — or retracted: the record is deleted, its IRI ceases to resolve, and **no tombstone** is created. Because identity is deterministic, a retracted tuple that is later re-proposed regains the same UUID by construction; there is no identity split and no way to "lose" an identity by retraction. + +**Promotion.** `draft → candidate` requires expert review (governance §4.2: rationale, sources, attestation of the reference points, canonical-form check). Promotion changes `status` only — it MUST NOT change identity fields, so the IRI survives promotion unchanged. From promotion onward the record is permanent and the tombstone rules of versioning apply. `candidate → active` and the tombstone states are unchanged. + +**Granularity.** References share their work's status (the compiler assigns `status` from the work record), so the promotion unit is a work together with its references; citation systems and mappings promote individually. A promoted record MUST NOT reference a draft `Work` or `CitationSystem` through its keys — systems and works are promoted before or together with the references that depend on them. + +**ID policy.** All registry records carry deterministic v5 identifiers at every stage. Provisionality is expressed by `draft` status, not by the ID algorithm; governance §6.3/6.4 is amended accordingly. + +## Consequences + +### Positive + +- The registry gains a reviewable, retractable tier without weakening any existing promise: permanence attaches at promotion and is absolute from there. +- Review-stage corrections cost nothing: no tombstone debt for data nobody ever relied on. +- One lifecycle connects the governance review tracks (§4) to the status model (§5): technical review gates entry, expert review gates permanence. +- The earlier wish to downgrade the unreviewed seed data below `candidate` becomes meaningful and lands as a follow-up. + +### Negative / trade-offs + +- Dump consumers MUST filter by `status` if they need only promoted records; a draft's presence in one release says nothing about the next. +- A retracted draft's IRI returns 404 rather than a tombstone page — link rot is possible for anyone who cited a draft despite the flagging. +- Re-proposal reuses the same UUID; consumers must not treat a reappearing id as continuity of curation history. +- Two texts (EN + legally binding DE governance) must be amended in sync. + +### Follow-up actions + +- [x] Compiler invariant: a promoted (`candidate`+) record MUST NOT reference a draft `Work`/`CitationSystem` (extend the tombstone invariant in `scripts/compile.ts`). +- [x] Draft rendering: banner, `noindex`, and search (Pagefind) exclusion on record pages (extend the `Tombstone.astro` pattern). +- [ ] Sitemap exclusion for draft record pages — Starlight's `@astrojs/sitemap` is unfiltered, so drafts are still listed. `noindex` covers the crawler contract for now. +- [x] Downgrade the current unreviewed seed data to `draft` in `textrefs/registry`. +- [ ] Optional CI persistence check: promoted ids diffed against the previous release dump. + +## Links + +- Related ADRs: ADR-0002 (deterministic identity from the semantic tuple — the identity/registration separation this builds on), ADR-0004 (supersedes this ADR: collapses the ladder to `draft` → `active`, removing `candidate`) +- Related issues / PRs: textrefs/textrefs.org#22, textrefs/textrefs.org#15 +- External references: governance regulation §4–§6 (`/association/governance/`) diff --git a/decisions/ADR-0004-lean-lifecycle.md b/decisions/ADR-0004-lean-lifecycle.md new file mode 100644 index 0000000..00b607b --- /dev/null +++ b/decisions/ADR-0004-lean-lifecycle.md @@ -0,0 +1,83 @@ +# ADR-0004: Lean record lifecycle — `draft` → `active` + +- **Status:** Accepted +- **Date:** 2026-08-04 +- **Deciders:** @maehr +- **Tags:** spec, governance + +## Context and problem statement + +ADR-0003 established the ladder `draft` → `candidate` → `active` → `deprecated` / `withdrawn` / `blocked`, and attached the identifier-persistence promise at the `draft → candidate` promotion (specification §11: "The persistence promise attaches at **promotion**: the first time a record is published at status `candidate` or higher"). That leaves `candidate` as a state that is permanent but not recommended. Nothing in the model distinguishes it operationally from `active` except that `active` also carries a recommendation (specification §12: `active` — "accepted and recommended for use"; `candidate` — "proposed but not yet accepted as stable"). A promoted-but-unrecommended tier is not a distinction TextRefs' review process or its consumers act on anywhere. + +The status enum is defined once, in `standard/schema/common.ts` (before this change: `Status = z.enum(['draft', 'candidate', 'active', 'deprecated', 'withdrawn', 'blocked'])`), and duplicated once, in `api/openapi.yaml:183-185`. No record anywhere in the registry is on `candidate`: every record in the `data/` submodule is `draft` (ADR-0003's own follow-up downgraded the seed data to `draft`, and nothing has since been promoted). Removing the state therefore needs zero data migration. + +`scripts/compile.ts:631` already keys its dependency invariant off `status !== 'draft'` rather than off `candidate` by name: + +```ts +// Analogously (ADR-0004): a promoted record — anything past `draft`, so +// anything that carries or once carried the persistence promise — MUST NOT +// depend on a record that is still retractable. +const isPromoted = (r: StatusRecord) => r.status !== 'draft'; +``` + +The logic survives a two-state ladder unchanged. The real cost of keeping the state is prose: specification §11–§12, the versioning maturity-ladder prose, identifier-syntax, get-started examples, CONTRIBUTING, and — because it is the legally binding text — the German governance regulation, which ADR-0003 itself records as amended in lockstep with the English original (`association/governance.md` §4.2, §5.1, §5.2, §5.3, §6, and its `de/` counterpart). + +## Decision drivers + +- A status vocabulary should have no state whose meaning must be explained by what it is _not_: `candidate` is defined by "not yet `active`", not by anything it uniquely permits or forbids. +- Promotion should be one reviewable event, not two: expert review under governance §4.2 already gates both "permanent" and "recommended" judgments together in practice. +- The identifier-persistence promise must stay absolute where it applies; ADR-0003's finding — that a promise with fuzzy edges is worse than a narrower hard one — argues against a state whose only content is "already permanent, not yet endorsed". +- ADR-0002's offline-computable identity (deterministic v5 UUIDs from `(work_key, citation_system_key, locator)`) must hold at every remaining stage; collapsing the ladder does not touch the identity seed. +- Additional citation systems (issue [#60](https://github.com/textrefs/textrefs.org/issues/60), ADR-0005) need to be introducible as drafts against an already-active work, which requires a clear per-record status _dependency_ rule rather than a total ordering of ladder rungs. + +## Considered options + +1. **Keep the three-state ladder — status quo.** The extra state keeps costing prose and review ceremony (two governance sections, two spec sections, a compiler comment, a fixture value) without buying a distinction anyone consumes: no tooling, no rendering, and no policy in this repository treats `candidate` differently from `active` except for the recommendation flag. +2. **Keep `candidate` but attach permanence at `candidate → active` instead of `draft → candidate`.** This makes `candidate` mutable, which contradicts ADR-0003's core finding that review-stage records must be cheap to correct (§ Ephemerality: "Draft records ... MAY be corrected ... or retracted ... without a tombstone"). Moving that ephemerality window one rung up just relabels which state absorbs the correction cost, and reintroduces the exact ambiguity ADR-0003 was written to remove — a published-but-uncommitted state with no name for its own promise. +3. **Collapse to `draft` → `active`.** ← chosen +4. **Rename `candidate` to something clearer** (e.g. `reviewed`, `accepted`). Cosmetic: the redundancy is structural — a promoted-but-unrecommended tier that nothing uses — not lexical. A better name does not give the state a purpose. + +## Decision + +We choose **Option 3**. The status ladder is `draft` → `active`, plus the tombstone states `deprecated`, `withdrawn`, `blocked`. The surviving mutable-review keyword is `draft`, not `candidate` — ADR-0003's ephemeral tier is unchanged, only the rung above it is removed. + +**`draft`.** Unchanged from ADR-0003: mutable, not recommended, excluded from the persistence policy (specification §11), retractable without a tombstone. + +**`active`.** Promotion out of `draft` now means _both_ "TextRefs recommends this record" _and_ "this identifier is now permanent". These were always judged together under governance §4.2 expert review; there is no longer a state that carries one without the other. One event, one gate: expert review, governance §4.2. + +**Corrections after promotion.** An incorrect `active` record is never deleted and its identity-defining fields are never mutated — that constraint is unchanged from specification §11. It is marked `deprecated` (retained, no longer recommended), `withdrawn` (erroneous or superseded), or `blocked` (rights, trust, or policy dispute), and carries `superseded_by` when a successor exists. Because identity is deterministic (ADR-0002), the corrected tuple mints a _new_ UUID at a new IRI; the old IRI keeps resolving as a tombstone page. This is the only correction path for a promoted record: no identity mutation, no deletion. + +**General dependency rule.** An `active` record MUST NOT depend on a `draft` record: an active `CanonicalReference` needs an active `Work` and an active `CitationSystem`; an active `MappingAssertion` needs an active subject `Work`. This restates `scripts/compile.ts:631`'s existing invariant in terms of the two-state ladder instead of "candidate or higher". ADR-0005 specialises this rule for the preferred-versus-additional citation-system distinction it introduces. + +**Governance.** `association/governance.md` (and its legally binding `de/association/governance.md` counterpart) lose the `candidate` row in §5.1 and §5.2; §4.2's promotion bullet becomes "promotion of records from `draft` to `active`"; §5.3's "Promoted IDs (status `candidate` or higher)" becomes "Active IDs"; §6 item 4 ("Provisional records are expressed through the `draft` status ... their IDs may disappear or change until promotion") follows without further change other than dropping any remaining `candidate` reference. Both texts are amended in the same change, as ADR-0003 required. + +## Consequences + +### Positive + +- One promotion event and one review gate, matching how governance §4.2 already treats the decision in practice. +- The status vocabulary is now fully explained by two live states plus three tombstones — no state defined by what it is not. +- No data migration: nothing in the `data/` submodule is on `candidate`. +- ADR-0005's per-record dependency rules become expressible in one sentence each, instead of needing to reason about ladder position. +- The spec carries `maturity: working-draft` (`/standard/versioning/`, "Unstable. Data model and prose may change without notice and without a version bump while the core is settled"), so this change needs no spec version bump. + +### Negative / trade-offs + +- Dump consumers reading the `Status` enum see one fewer value. Under `/standard/versioning/#semver-rules-for-data-packages` this is a breaking change for the data package's SemVer, exercised here under the pre-1.0 latitude ADR-0002 and ADR-0001 already established as acceptable. +- Anyone who read ADR-0003 must now read two ADRs (ADR-0003 plus this one) to know the current lifecycle model; ADR-0003 itself is not rewritten, only superseded. +- Two legally distinct texts (English `governance.md`, binding German `de/association/governance.md`) must be amended in sync again, repeating the coordination cost ADR-0003 already paid once. +- A future registry need for a "proposed but already permanent" tier would have to re-add a state rather than repurpose an existing one. + +### Follow-up actions + +- [x] Remove `candidate` from the `Status` enum in `standard/schema/common.ts` and from `api/openapi.yaml:185`. +- [x] Reword the compiler invariants and comments in `scripts/compile.ts` (e.g. the comment above line 631) to describe a `draft` → `active` ladder instead of "candidate or higher". +- [x] Update `src/lib/registry.fixture.ts`, whose fixture records currently carry `status: 'candidate'`. +- [x] Sweep the lifecycle prose: specification §11–§12, `/standard/versioning/`, `/standard/identifier-syntax/`, get-started examples, `CONTRIBUTING.md`, and its community-docs mirror. +- [x] Amend governance §4.2, §5.1, §5.2, §5.3, and §6 in both `association/governance.md` (English) and `de/association/governance.md` (binding German). + +## Links + +- Related ADRs: ADR-0002 (deterministic identity from the semantic tuple), ADR-0003 (superseded by this one), ADR-0005 (preferred citation system — builds on the dependency rule this ADR states) +- Related issues / PRs: textrefs/textrefs.org#22 +- External references: governance regulation §4–§6 (`/association/governance/`) diff --git a/decisions/ADR-0005-preferred-citation-system.md b/decisions/ADR-0005-preferred-citation-system.md new file mode 100644 index 0000000..64dc2ed --- /dev/null +++ b/decisions/ADR-0005-preferred-citation-system.md @@ -0,0 +1,92 @@ +# ADR-0005: Preferred citation system and qualified `/cite/` aliases + +- **Status:** Accepted +- **Date:** 2026-08-04 +- **Deciders:** @maehr, @stephenhart8 +- **Tags:** spec, data-model + +## Context and problem statement + +Issue [#60](https://github.com/textrefs/textrefs.org/issues/60), raised by **@stephenhart8**, observes that the alias shown on a reference page drops the citation system: Odyssey 1.5 renders `/cite/homer.odyssey/1.5/` even though the record's own "In context" block lists Work, Citation system, and Locator, and even though the citation system is one of the three fields that seed the reference UUID (`referenceUuid`, `scripts/compile.ts:323`, per ADR-0002). The compiler mints exactly one alias per reference, `` `${workKey}/${locator}` `` (`scripts/compile.ts:543`). + +Today the defect is unreachable rather than handled: `data/works/*.yaml` carries a single scalar `citation_system:` per work (`WorkSource`, `scripts/compile.ts:157`), so alias keys are unique by construction, and all 12 seed works are single-system. `setAlias` (`scripts/compile.ts:341`) throws when two references claim the same alias, so the current failure mode is a broken build, not a wrong redirect. The deeper consequence is that **a work under two citation systems cannot be represented at all** — even though `CanonicalReference.citation_system_key` is per-reference and `src/pages/id/work/[key]/index.astro:46` already derives and renders a _list_ of a work's systems (`usedSystemKeys` / `usedSystems`). + +That matters because the roadmap's plan to grow the registry across theology, classics, philosophy, and law runs directly into works where the same locator string is valid under two systems and denotes different passages: Psalms under Masoretic versus Septuagint/Vulgate versification (`Ps.23.1`), the Qur'an under Cairo versus Flügel verse numbering, Aristotle under Bekker (`1094a1`) and book/chapter (`1.1`). A bare `{work}/{locator}` alias there does not merely under-specify — it silently resolves to the wrong passage, which is worse than a 404. + +Quote @stephenhart8's own framing from #60: always adding the citation system to the alias would solve the ambiguity but "might complexify 90% of the usage of the passages, as it is often implied which system is used", and their proposal is a preferred citation system so the common case stays `homer.odyssey/1.5` while non-preferred systems carry the system segment. + +## Decision drivers + +- Short aliases must stay memorable and hand-typeable — that is the entire purpose of `/cite/` (`/get-started/url-layout/`). +- An alias must never silently denote a different passage. +- Reference identity (ADR-0002) must not move. +- The registry must be able to represent multi-system works. +- Which system is "preferred" is a curatorial claim that needs a review path, not a hard-coded rule. +- Additional systems are fallback functionality and must be introducible as drafts against an already-active work (ADR-0004's dependency rule). + +## Considered options + +1. **Status quo** — bare alias only. Blocks multi-system works entirely; `setAlias` turns the ambiguity into a build failure with no way forward. +2. **Always qualify; drop bare aliases** — `/cite/{work}/{system}/{locator}` for everything. Unambiguous and needs no curatorial judgment, but breaks every existing short URL and defeats the point of a memorable shortcut: readers know `plato.republic/514a`, not `plato.republic/stephanus/514a`. +3. **Bare for preferred, qualified only for others** — issue #60 as written. Minimal change, but a reference under the preferred system then has _no_ unambiguous alias at all, which is the form you would want to cite or share precisely when a work has competing systems. +4. **Build-time disambiguation pages** — a bare alias with more than one candidate renders a chooser. No curatorial decision needed, but a URL that resolves today would silently become a chooser page tomorrow when a second system is added — a regression on an already-shared link. +5. **Qualified always, bare when preferred.** ← chosen + +## Decision + +We choose **Option 5**. + +- A new **mandatory** `Work.preferred_citation_system_key` (flat key syntax), published in the JSON-LD record and in the `works.jsonl` dump — consumers can see and reuse it. +- Every reference gets `/cite/{work}/{system}/{locator}/`. The bare `/cite/{work}/{locator}/` is minted only for the work's preferred system. +- Collisions become structurally impossible rather than merely detected: a qualified alias is keyed by the same tuple that seeds the UUID, and at most one system per work is preferred. +- The grammar is unambiguous by segment count only because keys (`FlatKey`, `^[a-z0-9][a-z0-9._-]*$`) and locators both exclude `/`. Locators containing `/` are therefore rejected before alias minting — a new compiler invariant, since no current `locator_regex` allows one but nothing stated the constraint. +- **Alias permanence.** `/cite/` aliases are presentational. They may be added, removed, or **retargeted**. Changing a work's preferred citation system changes what an existing bare alias resolves to, _including for active works_. This is allowed and must be documented prominently; there is no alias ledger and no freezing. Qualified aliases and `/id/ref/{uuid}` identifiers are never retargeted. Governance §6 item 6 already says citation URLs are aliases; this ADR makes retargeting explicit. +- **UUID contract.** Adding an additional citation system creates new reference identities from new `(work, citation system, locator)` tuples. **No existing reference UUID changes.** It adds qualified aliases and may change bare aliases — acceptable precisely because aliases are not permanent. +- **Status dependency rules**, specialising ADR-0004's general rule: + + ```text + An active reference requires: an active work, AND + an active citation system for its citation_system_key. + An active work requires: an active preferred citation system. + An active work MAY additionally carry draft citation systems and draft references. + The status of a fallback system never downgrades the work. + ``` + +- **Authoring.** The work source keeps `citation_system:` as the preferred system and gains an optional `additional_systems:` list, each entry carrying its own citation system, resolvers, references, and an optional `reference_status:`. `reference_status` on the primary block defaults to the work's status; on an `additional_systems` block it defaults to **`draft`**, never to the work's status — a newly added fallback system must not silently inherit `active`. Resolver URL templates stay scoped to their block because template variables come from that system's `locator_regex` capture groups. +- Preference is expressed positionally in the source (the top-level block is the preferred one), so it cannot drift out of sync with the systems a work actually uses, while the compiled record carries `preferred_citation_system_key` explicitly for consumers. + +## Consequences + +### Positive + +- Existing short URLs keep working; every reference gains a stable unambiguous alias; alias collisions become structurally impossible. +- The field is identity-neutral (`Work` IRIs are key-derived, reference UUIDs unaffected), so nothing in ADR-0002 moves. +- Multi-system works become representable, and fallback systems can be reviewed independently of the work. + +### Negative / trade-offs + +- A mandatory field means a work with genuinely competing systems must pick one rather than abstain — the mitigation is documentary (share the qualified form when precision matters; cite `/id/`). +- Bare aliases can be retargeted, so anyone who treated `/cite/` as permanent is relying on something never promised. +- Adding the required field is breaking for `works.jsonl` consumers. +- Roughly twice as many alias entries are compiled. +- Who decides "preferred" is a curatorial judgment; the review path is expert review under governance §4.2, informed by how frequently a system is used in a scholarly community, as @stephenhart8 suggested — a decision input, not a rule. + +### Follow-up actions + +- [ ] Required `preferred_citation_system_key` in `standard/schema/work.ts`. +- [ ] Zod validation of the YAML source shape, including duplicate-system rejection. +- [ ] Per-block reference emission and alias minting in `scripts/compile.ts`. +- [ ] Reject `/` in locators; new compiler invariant. +- [ ] JSON-LD context term and OpenAPI `Work` schema. +- [ ] Disambiguate duplicate locators in the work page, the paginated reference list, the reference page title, and the rendered citation. +- [ ] Document `additional_systems:` and both alias forms. +- [ ] Compiler test coverage. +- [ ] A note in `textrefs/registry`'s `data/AGENTS.md`. +- [ ] Deferred: real multi-system registry data (needs a curated second profile and attested reference points). +- [ ] Deferred: per-community/per-language preference. + +## Links + +- Related ADRs: ADR-0002 (deterministic identity), ADR-0004 (lifecycle and the general dependency rule this specialises) +- Related issues / PRs: textrefs/textrefs.org#60 — raised by @stephenhart8, whose preferred-citation-system proposal this ADR adopts +- External references: `/get-started/url-layout/`, governance §4.2 and §6 diff --git a/decisions/ADR-0006-mapping-relation-vocabulary.md b/decisions/ADR-0006-mapping-relation-vocabulary.md new file mode 100644 index 0000000..4d938a2 --- /dev/null +++ b/decisions/ADR-0006-mapping-relation-vocabulary.md @@ -0,0 +1,116 @@ +# ADR-0006: MappingAssertion relation vocabulary — `alternateOf` and `isReferencedBy` + +- **Status:** Proposed +- **Date:** 2026-08-11 +- **Deciders:** @maehr +- **Tags:** spec, data-model + +## Context and problem statement + +`MappingAssertion.relation` is a two-value enum — `z.enum(['exactMatch', 'closeMatch'])` (`standard/schema/mapping-assertion.ts:21`) — published as `skos:exactMatch` and `skos:closeMatch` (`public/contexts/v1.jsonld:40-47`). Two community issues filed by @stephenhart8, both marked breaking, argue that both values are wrong for the way the registry actually uses them. (Both issues name `v0.2.0-draft`; the repository is on `v0.1.0-draft`, `ROADMAP.md:7` and `specification.md:9`. See _Versioning_ below.) + +**[#59](https://github.com/textrefs/textrefs.org/issues/59)** — `skos:closeMatch` is the wrong property for linking a Work to a _page about_ it. Every one of the 12 `closeMatch` mappings in `data/works/*.yaml` is a Wikipedia article URL: a document that describes the work, not a weaker identifier for it. @stephenhart8 proposed `dcterms:isReferencedBy`; @julsraemy agreed. + +**[#58](https://github.com/textrefs/textrefs.org/issues/58)** — `skos:exactMatch` is a category error for Work ↔ Wikidata co-reference. SKOS mapping properties have domain and range `skos:Concept`; a `Work` is not a `skos:Concept`. @stephenhart8 proposed `owl:sameAs`. @julsraemy objected that `owl:sameAs` entails indiscernibility, symmetry and transitivity into the whole `sameAs` closure — pulling Wikidata's fused VIAF/GND identities and all their assertions in under TextRefs' semantics — and that `schema:sameAs` fails in the opposite direction, being underspecified and itself defined around a _reference web page_, which is closer to #59's relation. The thread converged on three acceptable candidates (`skos:exactMatch` retained, a minted `equivalent` à la Linked Art, or `prov:alternateOf`), and both @julsraemy and @stephenhart8 ruled out `skos:exactMatch` as genuinely wrong rather than cosmetically imprecise. + +The two issues are one change. `relation` appears in the authoring shape (`scripts/compile.ts:133`), the record schema, the Work projection (`standard/schema/work.ts:24-28`), the published context, the OpenAPI spec (`api/openapi.yaml:288`), eight documentation files, and — decisively — **the deterministic UUID seed**: `subject \n relation \n target.identifier` (`src/content/docs/standard/identifier-syntax.md:114-122`, ADR-0002). Changing a relation value re-mints every affected mapping IRI. Settling the two issues separately would mean two breaking spec bumps, two rounds of edits to the same paragraphs, two identifier churns, and an interim vocabulary in which one relation is SKOS and its sibling is not — precisely the incoherence #58 objects to. + +Timing forces the question now rather than later. Every record in the `data/` submodule is `status: draft` and the repository carries no tags. Under ADR-0004 a `draft` identifier carries no persistence promise and may be retracted without a tombstone, so re-minting mapping UUIDs today costs nothing. After the first promotion to `active` it costs a migration, permanently. + +## Decision drivers + +- A published relation must be semantically defensible against the domain and range its vocabulary actually declares — the objection in #58 is that `skos:exactMatch` fails this, not that it is imprecise. +- The relation must not entail more than TextRefs can vouch for across independently modelled graphs. TextRefs asserts "same real-world referent, go here for more", not co-reference in all contexts. +- One breaking change, not two. Both issues target `v0.2.0-draft` and touch the same enum, the same context block, the same UUID seed, and the same documentation paragraphs. +- ADR-0002's seed shape (`subject`, `relation`, `target.identifier`) must survive unchanged; only the seed _values_ may move. +- ADR-0004's draft-only registry makes identifier churn free exactly once. This decision has to land inside that window. +- The vocabulary must be selectable by an author without judgement calls about confidence — see the decision below. + +## Considered options + +### For the Work ↔ external entity relation (#58) + +1. **Keep `skos:exactMatch`.** Zero work, and @julsraemy initially rated the domain/range problem cosmetic. Rejected: both issue participants ultimately agreed it is a real category error, not a typing artefact, and retaining it leaves the standard asserting a concept-mapping property between things that are not concepts. +2. **`owl:sameAs`.** The de-facto LOD idiom for cross-KG entity links, as @stephenhart8 noted. Rejected: it entails indiscernibility — every assertion about one IRI holds for the other, in both directions — and its transitive closure imports whatever else the target has been fused with. TextRefs cannot guarantee that an external item sits at the same level of abstraction as its own Work. +3. **`schema:sameAs`.** Widely deployed and JSON-LD-friendly (@maehr). Rejected: underspecified, so consumers read it differently, and its own definition is framed around a reference web page — which makes it a candidate for #59's relation, not this one. +4. **Mint a TextRefs `equivalent` property**, following Linked Art's precedent (linked-art/linked.art#307). Semantically exact and fully under our control. Rejected for now on cost: `https://textrefs.org/ontology#` is declared in the context (`public/contexts/v1.jsonld:3`) but nothing is published there, so a minted term would be the first relation whose definition is undereferenceable and normatively load-bearing. +5. **`prov:alternateOf`.** ← chosen + +### For the Work ↔ page-about-it relation (#59) + +1. **Keep `skos:closeMatch`.** Rejected: a Wikipedia article is not a weaker identifier for the work, it is a document about it. The relation is the wrong kind, not the wrong strength. +2. **`dcterms:isReferencedBy`.** ← chosen +3. **`schema:subjectOf` / `foaf:page`.** Viable but narrower; `dcterms:isReferencedBy` already has a sibling in the context (`superseded_by` → `dcterms:isReplacedBy`) and no competing reading. + +## Decision + +The relation vocabulary is selected by **what the target is**, never by how confident the author feels. Two values: + +| `relation` | Published as | Use when the target is | +| ---------------- | ------------------------ | ------------------------------------- | +| `alternateOf` | `prov:alternateOf` | another entity denoting the same work | +| `isReferencedBy` | `dcterms:isReferencedBy` | a document or page _about_ the work | + +**`alternateOf` → `prov:alternateOf`** (#58). Per @maehr on the thread: "I guess `prov:alternateOf` is more commonly used, let's go with that", closing a discussion in which @julsraemy ("I'd be really happy with `prov:alternateOf` or `la:equivalent`") and @stephenhart8 ("both `prov:alternateOf` and `la:equivalent` seem to do the job") had already converged. + +`prov:alternateOf` relates two `prov:Entity` instances that present the same thing from different perspectives or at different levels of abstraction. Under PROV-CONSTRAINTS it is reflexive, symmetric and transitive — but, unlike `owl:sameAs`, **it does not entail indiscernibility**. Alternates may carry different attributes and sit at different levels of abstraction; that is what the property is _for_. This is the exact gap @julsraemy identified between what TextRefs wants to say and what `owl:sameAs` says, and it means the Wikidata-item objection (edition- and translation-level facts hanging off `Q35160`) does not bite: those facts are not licensed onto the TextRefs Work. + +**`isReferencedBy` → `dcterms:isReferencedBy`** (#59). The thread's one objection — that `dcterms:isReferencedBy` is "intended to be used with non-literal values" — only applies if the value is emitted as a string literal. Every mapping term in the published context already carries `"@type": "@id"`, which makes values node references, exactly as `superseded_by` → `dcterms:isReplacedBy` already does (`public/contexts/v1.jsonld:78-81`). The new term is declared the same way. Recorded here so the objection does not resurface. + +**`closeMatch` is removed entirely.** After the two reclassifications above, no SKOS mapping property is asserted with a `Work` as its subject — which is what #58's category argument actually requires. That leaves `closeMatch` with no valid use at all, because every one of its four documented jobs is either reassigned or unauthorable: + +- Work → proxy page (`data/AGENTS.md:14`, `mappings-and-resolver-targets.md:35`) → now `isReferencedBy`. +- Work → whole edition, scan, or digital object, i.e. scope mismatch (`mappings-and-resolver-targets.md:119`) → a Work-subject SKOS assertion, so it falls to the same category objection. Such a target is either an alternate presentation of the work (`alternateOf`) or a resolver target, which is a different record type entirely; it is not a mapping. +- Disputed authorship attribution (`authoring.md:126`, `data/AGENTS.md:41`) → `closeMatch` never expressed attribution uncertainty in the first place, and the guidance matches no record in the registry. Withdrawn without replacement; see follow-ups. +- System ↔ system divergent versification (`specification.md:368`, `how-it-works.md:128`, `mappings-and-resolver-targets.md:117`) → the one case where `skos:closeMatch` is semantically defensible, since two citation systems genuinely are concept-scheme-like vocabularies of locators. But `MappingAssertion.subject` MUST be a Work IRI (`standard/schema/mapping-assertion.ts:6-11,20`), so this assertion cannot be authored at all. Specification §13 has been describing a record the schema forbids — a contradiction that predates this ADR. + +Retaining `closeMatch` for that last case was considered and rejected. It would leave a value in a published enum and in `api/openapi.yaml:288` that no valid record may carry, which is a trap for implementers reading the vocabulary as a menu. The asymmetry is decisive: **removing an enum value is breaking, adding one is not.** This change is already breaking, so the removal is free; re-introducing `closeMatch` once `subject` is widened to admit a CitationSystem IRI is an additive, non-breaking change at that point. Keeping it costs a permanently dead value; dropping it costs nothing. + +**Work projection.** The compiler's read-only projection onto `Work` becomes `alternateOf` and `isReferencedBy` (`standard/schema/work.ts:24-28`). + +**Identity.** ADR-0002's seed shape is unchanged, and the mapping namespace UUID `f16bb214-4241-549d-ad41-7b011f02befb` stays frozen (`identifier-syntax.md:106-112`). Only the seed _values_ change, re-minting all 24 existing mapping IRIs. This is deliberate and is why the change lands now, while ADR-0004 makes draft identifiers retractable without tombstones. Both axes ship as a single breaking change. + +**Versioning.** No spec version bump. The standard stays `v0.1.0-draft` (`ROADMAP.md:7`, `specification.md:9`) and carries `maturity: working-draft`, which `/standard/versioning/` defines as "Unstable. Data model and prose may change without notice and without a version bump while the core is settled." ADR-0004 was itself a breaking enum change and invoked exactly this clause to decline a bump; this ADR follows that precedent rather than inventing a `v0.2.0-draft` the repository has never been on. The issues' `v0.2.0-draft` label reflects their authors' expectation, not repository state. The **data package** is a separate artefact and does take a breaking SemVer bump under `/standard/versioning/#semver-rules-for-data-packages`, as ADR-0004's did. + +## Consequences + +### Positive + +- No relation in the vocabulary asserts a property outside its declared domain and range. +- The vocabulary is chosen by target kind, so authoring needs no confidence judgement — the ambiguity that let one `closeMatch` value do four unrelated jobs is gone by construction. +- `prov:alternateOf` gives cross-KG reconciliation without OWL identity: no indiscernibility, no imported `sameAs` closure. +- One breaking change instead of two, with no interim state in which one relation is SKOS and its sibling is not. +- `validate-data.ts` independently recomputes every mapping UUID from `[subject, relation, target.identifier]` (`scripts/validate-data.ts:71-75`), so the re-mint is verified by the existing build rather than by inspection. + +### Negative / trade-offs + +- All 24 existing mapping IRIs change. Any external reference to `https://textrefs.org/id/mapping/{uuid}` breaks. Permitted only because every record is `draft` under ADR-0004; this window does not reopen. +- Breaking for the data package's SemVer under `/standard/versioning/#semver-rules-for-data-packages`, exercised under the pre-1.0 latitude ADR-0001 and ADR-0002 already established. +- Specification §13's divergent-versification mechanism is left with no vocabulary at all until `subject` is widened. This makes an existing latent contradiction visible rather than creating a new one — the record §13 describes was never authorable — but the spec must now say so plainly instead of implying a usable relation. +- The registry loses its documented mechanism for disputed attribution without gaining a replacement in this ADR. +- `prov:alternateOf` is less immediately recognisable to consumers than `owl:sameAs`; naive `sameAs`-following clients will not traverse it. That is the intended behaviour, but it is a real interoperability cost. +- A PROV vocabulary appears in the context solely for this one term, adding a namespace whose other machinery TextRefs does not use. + +### Follow-up actions + +Line anchors below are against `feat/preferred-citation-system` (PR [#63](https://github.com/textrefs/textrefs.org/pull/63)), which this work stacks on — see _Sequencing_ under Links. + +- [x] Replace the enum with `z.enum(['alternateOf', 'isReferencedBy'])` in `scripts/source-schema.ts:73` and `standard/schema/mapping-assertion.ts:21`, keeping the two in lockstep. ADR-0005's Zod `MappingSource` makes an unknown relation fail at parse time; before it, the authoring shape was an unvalidated TypeScript type. +- [x] Replace the hard-coded two-way branch in `scripts/compile.ts:447-455,474-475` with an enum-keyed accumulator, so a further relation needs no new branch; update the `// Direct SKOS mapping edges` comment at `:447`, which no longer describes the projection at all. +- [x] Add `"prov": "http://www.w3.org/ns/prov#"` to the namespace block in `public/contexts/v1.jsonld:2-7`; replace the `exactMatch` and `closeMatch` terms with `alternateOf` → `prov:alternateOf` and `isReferencedBy` → `dcterms:isReferencedBy`, both `"@type": "@id"`. +- [x] Rename the projection fields in `standard/schema/work.ts:33-34` and rewrite the comment at `:31`, which names SKOS explicitly. +- [x] Reclassify all 12 Wikidata mappings to `alternateOf` and all 12 Wikipedia mappings to `isReferencedBy` in `data/works/*.yaml`, via a PR to `textrefs/registry`; bump each `modified`. +- [ ] Confirm in the spec that `isReferencedBy` targets remain lookup aliases (`scripts/compile.ts:518`, `setAlias(aliases, mapping.identifier, workIri)`). Recommended: yes — the alias table is a lookup convenience, not an identity claim — but it must be stated rather than inherited. +- [x] Sweep the prose: `specification.md:131,140,247,264,373`, `json-ld.md:31-36,72-73`, `identifier-syntax.md:122` (the seed's enumerated literal values), `mappings-and-resolver-targets.md:35,41,117,119,130`, `authoring.md:38,126`, `how-it-works.md:97,128`, `data/AGENTS.md:14,41`, and `api/openapi.yaml:220-230,297`. +- [x] Decide what the re-mint checklists mean now: `README.md:135`, `.github/PULL_REQUEST_TEMPLATE.md:39` and `data/.github/PULL_REQUEST_TEMPLATE.md:19` link a withdrawn record to its successor with `relation: exactMatch`. `alternateOf` fits (same referent, different presentation), but `superseded_by` → `dcterms:isReplacedBy` already covers succession — this may be a redundant instruction rather than one to translate. +- [x] Extend `scripts/compile.test.ts` (260 lines from ADR-0005, no mapping coverage): projection grouping, tombstone exclusion, `mappingUuid` stability for a fixed triple, and rejection of an out-of-enum relation. Add an `isReferencedBy` mapping alongside `src/lib/registry.fixture.ts:93`. +- [ ] **Separate ADR:** widen `MappingAssertion.subject` to admit a CitationSystem IRI, then re-introduce `closeMatch` as an additive change. Until then specification §13 must state that the equivalence it describes is not yet expressible. +- [ ] **Separate ADR or issue:** how to express disputed authorship attribution, now that `closeMatch` no longer pretends to. +- [ ] Publish an ontology stub at `https://textrefs.org/ontology#`. Pre-existing debt (`tr:Work`, `tr:relation`, `tr:locator` are all undereferenceable), not created here, but option 4 above was rejected partly on its account. + +## Links + +- Related ADRs: ADR-0002 (UUID seed — the reason a relation change re-mints identifiers), ADR-0004 (draft-only lifecycle — the reason the re-mint is free now, and the precedent for a breaking change without a spec version bump), ADR-0005 (introduces the Zod authoring schema this change extends) +- Related issues / PRs: textrefs/textrefs.org#58, textrefs/textrefs.org#59 +- **Sequencing:** this ADR and its implementation stack on PR [#63](https://github.com/textrefs/textrefs.org/pull/63) (ADR-0005, `feat/preferred-citation-system` → `staging`), not directly on `staging`. #63 introduces `scripts/source-schema.ts` — the runtime validation this change's enum needs — and `scripts/compile.test.ts`, where the missing mapping coverage belongs. Basing on `staging` instead would mean editing an unvalidated TypeScript type and then resolving the same conflict when #63 merges. Retarget this PR's base to `staging` once #63 lands. +- External references: [PROV-O `alternateOf`](https://www.w3.org/TR/prov-o/#alternateOf), [PROV-CONSTRAINTS §alternate](https://www.w3.org/TR/prov-constraints/#term-alternate), [DCMI Metadata Terms `isReferencedBy`](https://www.dublincore.org/specifications/dublin-core/dcmi-terms/#http://purl.org/dc/terms/isReferencedBy), [SKOS mapping properties](https://www.w3.org/TR/skos-reference/#mapping), linked-art/linked.art#307 diff --git a/decisions/ADR-0007-alternative-labels-for-works.md b/decisions/ADR-0007-alternative-labels-for-works.md new file mode 100644 index 0000000..847cbeb --- /dev/null +++ b/decisions/ADR-0007-alternative-labels-for-works.md @@ -0,0 +1,101 @@ +# ADR-0007: Alternative labels on `Work` — one flat list, published as `skos:altLabel` + +- **Status:** Accepted +- **Date:** 2026-08-24 +- **Deciders:** @maehr +- **Tags:** spec, data-model + +## Context and problem statement + +A `Work` carries exactly one name. `standard/schema/work.ts:22` declares `preferred_label` as a plain string, and `public/contexts/v1.jsonld:16` maps it to `skos:prefLabel`. Nothing else names a work. There is no `alt_label`, no `abbreviation`, no `alternateName`, and no language-tagged label anywhere in `standard/`, `scripts/`, `data/`, or the published context. `alternateOf` is not a label field: it holds IRIs of other entities, the compiler derives it from `MappingAssertion`s, and an author never writes it (ADR-0006). + +The consequence shows in the registry browser. Before this change, `src/pages/reg/index.astro:71` built its filter key from `preferred_label` and `key` only (anchor against `staging` at `8bcfdca`), and the client filter — `src/pages/reg/index.astro:115-118` — is a plain substring match on that attribute. A scholar who typed `NE`, `EN`, or `Nikomachische Ethik` found nothing. The registry holds Aristotle's _Nicomachean Ethics_, but only under that one English string. The same gap hit `PI` for the _Philosophical Investigations_, `LXX` for the Septuagint, and every German, French, or Latin title a scholar uses by habit. + +Two documents already promised the fix and named a field that did not exist: `src/content/docs/get-started/authoring.md:124` and `data/AGENTS.md:36` both said "alt-names belong in a future `alt_labels` field". The promise was two documents old and never had a schema behind it. + +The registry is small today, so the cost is small. The roadmap plans growth across theology, classics, philosophy, and law. Works in those fields carry many established short forms. Search by one canonical English label does not scale to that. + +This adds a field to a published record shape, so it needs an ADR rather than a `standard:` erratum. [#85](https://github.com/textrefs/textrefs.org/issues/85) laid out the four options below and recommended option 2. + +## Decision drivers + +- The registry browser must find a work by the name a scholar actually types, which in classics and theology is usually an abbreviation, not the English title. +- Labels must never reach identity. ADR-0002 fixes the reference UUID seed at `work_key`, `citation_system_key`, `locator`; no label appears in it, and none may be added. +- The change must be additive. Every existing record lacks the field, and the build must pass with no submodule data change. +- No new namespace. `skos` is already declared at `public/contexts/v1.jsonld:4` and already carries `preferred_label`. +- The language question applies to `preferred_label` too. Answering it for one field alone would leave the two inconsistent and make the eventual fix breaking for both. + +## Considered options + +1. **Do nothing.** Keep one label per work; tell clients to search the `preferred_label` string. Rejected: it leaves the registry unusable for a scholar who knows a work by its abbreviation, which is the common case in the fields the roadmap targets. +2. **One flat list of plain strings**, published as `skos:altLabel`. ← chosen +3. **Language-tagged label objects** — `{ value: string, language?: string }[]`. `LanguageTag` already exists at `standard/schema/common.ts:41-46` for `ResolverTargetEntry`, so the validator is available. Rejected for now, not on merit: it is the better end state, but it leaves `preferred_label` a bare string, and the inconsistency means the language question should be answered for both fields at once. See _Open questions_. +4. **Separate `alternative_labels` and `abbreviations`.** More expressive, because a search UI can give an abbreviation an exact match and a full title a substring match. Rejected: it doubles the authoring burden and the boundary is not clear. `LXX` is an abbreviation. `Septuaginta` is an alternate title. `Sept.` is arguable, and an author should not have to decide. + +## Decision + +We choose **option 2**. `Work` gains an optional `alternative_labels: string[]`, published as `skos:altLabel` with `"@container": "@set"`. + +The field is named **`alternative_labels`**, not the `alt_labels` that #85 and the two stale documents used. The registry spells its terms out — `preferred_label`, `preferred_citation_system_key`, `citation_system_key` — and an abbreviated key inside a field whose whole purpose is abbreviations reads as an accident. + +**Identity.** An alternative label is never a UUID seed input (ADR-0002). Adding, editing, or removing one MUST NOT move an identifier. `scripts/compile.test.ts` locks this: compiling the same work under two different label lists yields identical reference IRIs. This rule is non-negotiable and is stated normatively in `specification.md` §6. + +**Homonymy.** Two different works MAY claim the same alternative label. `Ethics` fits Aristotle and Spinoza. The compiler allows this and prints nothing. Homonymy is a fact about titles, not an authoring error, and the correct answer for a search field is two hits, not a rejected build. Consumers MUST treat such a match as ambiguous. + +**Authoring.** Inside one work, entries MUST be unique and MUST NOT repeat the `preferred_label`. An empty list is rejected too: omit the key instead. This keeps the uniqueness rule strictly local, which is what makes the homonymy allowance above coherent. + +These rules are enforced **twice**, because the two schemas answer different questions. `WorkSource.superRefine` (`scripts/source-schema.ts:191-207`) rejects the authored YAML at parse time with a field path, beside the duplicate-citation-system check. `Work.superRefine` (`standard/schema/work.ts:59-75`) rejects the compiled record, and it is the canonical schema the standard publishes — `scripts/validate-data.ts:36` and `scripts/compile.ts:519` both run it. Enforcing only the authoring shape would let a hand-built or third-party record carry a duplicate while still validating against the published schema, which would make the normative sentence in `specification.md` §6 unenforceable. The two report the same two messages, so a violation reads the same whichever layer catches it. + +**Scope.** `CitationSystem` does not get the field in this ADR, even though it has the same problem — `Bekker` and `Stephanus` are the names scholars use, not the registry's `preferred_label` strings. One decision per ADR. See _Open questions_. + +**Versioning.** No spec version bump. The standard stays `v0.1.0-draft` (`ROADMAP.md:7`, `specification.md:9`) and carries `maturity: working-draft`, which `/standard/versioning/` defines as "Unstable. Data model and prose may change without notice and without a version bump while the core is settled." ADR-0004 and ADR-0006 both invoked this clause; this ADR follows that precedent. The **data package** is a separate artefact and takes a **minor** SemVer bump at the next release under `/standard/versioning/#semver-rules-for-data-packages` — "Compatible new fields require a minor version increment" — which is automatic, since the compiler reads the version from `package.json`. + +## Consequences + +### Positive + +- The registry browser finds a work by abbreviation and by translated title. This needed no client script change: the labels are folded into the existing `data-reg-item` haystack. +- The field flows into `dist/dump/works.jsonl`, `/reg/works.json`, and `/id/work/{key}.json` with no further edit. `scripts/compile.ts:786-790`, `src/lib/collection.ts:11-15`, and `src/pages/id/work/[key].json.ts:15` all serialize the whole record rather than a field whitelist. +- `skos:altLabel` is standard vocabulary paired with the `skos:prefLabel` already in the context, so consuming tools understand it without documentation and no new namespace is added. +- The two stale `alt_labels` promises can finally be honoured. +- Additive and optional, so no existing record changes and no identifier moves. The gate that would have caught an omission — `scripts/validate-data.ts:81-114`, which fails on any emitted key absent from the context — passes. + +### Negative / trade-offs + +- Every work record grows an optional field that reviewers must check for accuracy. An alternative label is a curatorial claim, like a mapping, but unlike a mapping it carries no `source` and no `status`. It is asserted by the record and reviewed only in the pull request. +- Plain strings carry no language tag, so `Nikomachische Ethik` and `Ethica Nicomachea` sit in the same undifferentiated list. A client cannot filter labels by language or render the right one for its locale. +- If a later ADR moves to language-tagged objects, the field shape breaks for existing consumers. Deferring the question is the cost paid for shipping search in one release. +- Allowing cross-work duplicates means the browser can return two hits for one query with nothing to disambiguate them beyond the `key` shown in the row. That is correct, but it is a UI problem this ADR does not solve. +- Nothing checks `public/contexts/v1.jsonld` against its hand-maintained copy at `src/content/docs/standard/json-ld.md:58`. This ADR adds one more term that can drift between them. + +### Follow-up actions + +- [x] Add `alternative_labels: z.array(z.string().min(1)).optional()` to `WorkBase` in `standard/schema/work.ts`, after `preferred_label`. +- [x] Add `"alternative_labels": { "@id": "skos:altLabel", "@container": "@set" }` to `public/contexts/v1.jsonld`, and the same term to the copy in `src/content/docs/standard/json-ld.md`. +- [x] Add the field to the strict `work` block of `WorkSource` in `scripts/source-schema.ts` with `.min(1)`, plus the uniqueness check in its `superRefine`. Mirror both in the canonical `Work` schema, so the published record shape enforces what the specification asserts. +- [x] Project the field onto the compiled record in `scripts/compile.ts`, following the conditional-spread pattern the `creators` field uses. The record is built from an explicit field list, so an unlisted key is silently dropped. +- [x] Mirror the field on the `Work` schema in `api/openapi.yaml`, as an optional array carrying `minItems: 1` and `uniqueItems: true`, so a downstream client validates against the same constraints. +- [x] Fold the labels into the browser filter key in `src/pages/reg/index.astro`, and render them on `src/pages/id/work/[key]/index.astro`. +- [x] Update `specification.md` — the mermaid `class Work` block, the `plato.republic` example, the `Optional:` list, and one normative paragraph in §6 stating the identity and homonymy rules. +- [x] Replace the `alt_labels` promise in `src/content/docs/get-started/authoring.md` with a real `### alternative_labels` section. +- [x] Add the field to the fixture work in `src/lib/registry.fixture.ts`, so `npm run build:fast` exercises both the record page and the filter without submodule data. +- [x] Extend `scripts/compile.test.ts`: projection, omission, empty list, in-work duplicate, a label equal to `preferred_label`, a shared label across two works, and identifier stability under a label change — through the YAML path, and again directly against the canonical `Work` schema. +- [ ] Fix the stale `alt_labels` promise at `data/AGENTS.md:36`. That file lives in `textrefs/registry`, so it needs a pull request to the submodule repository. +- [ ] Seed alternative labels on `data/works/*.yaml` in a `textrefs/registry` pull request — `NE`/`EN` for the _Nicomachean Ethics_, `PI` for the _Philosophical Investigations_ — then bump the submodule pointer here. Follow the workflow in `CONTRIBUTING.md:135-140`. +- [ ] **Separate ADR or issue:** disambiguate a duplicate hit in the registry browser. Two works sharing a label is legal by this decision, and the row shows only the `key`. +- [ ] **Separate ADR:** language tagging for labels, answered for `preferred_label` and `alternative_labels` together. See _Open questions_ 1. + +## Open questions + +Recorded, not answered here. + +1. **Plain strings now, or language-tagged objects?** Deferring costs a breaking record-shape change later: consumers reading `alternative_labels` as `string[]` would have to move to `{ value, language }[]`. Answering it now would leave `preferred_label` a bare string and the two fields inconsistent, and fixing that inconsistency is itself breaking. The question belongs to both fields at once. +2. **One flat list, or separate `abbreviations` and alternate titles?** A search UI treats them differently — an abbreviation deserves an exact match, a full title a substring match. Option 4 above rejected the split on authoring cost, not on merit. Revisit if the browser gains ranked search. +3. **Does `CitationSystem` need the same field?** It has the same problem: `Bekker` and `Stephanus` are the names scholars use. Out of scope for v1. If the answer is yes, it is additive and non-breaking at that point. +4. **Is there an upper bound on the number of labels per work?** None is enforced. A work with thirty labels would bloat the filter haystack and the dump without an obvious benefit. No evidence yet on where a sensible limit sits. + +## Links + +- Related ADRs: ADR-0002 (the UUID seed — the reason a label can never move an identifier), ADR-0005 (introduces the Zod authoring schema this field extends), ADR-0006 (`alternateOf` is a mapping projection, not a label field) +- Related issues / PRs: textrefs/textrefs.org#85 +- External references: [SKOS `altLabel`](https://www.w3.org/TR/skos-reference/#labels), [JSON-LD `@container: @set`](https://www.w3.org/TR/json-ld11/#sets-and-lists) diff --git a/lychee.toml b/lychee.toml index d309414..f2f8688 100644 --- a/lychee.toml +++ b/lychee.toml @@ -33,6 +33,7 @@ exclude = [ "^https?://([a-z0-9-]+\\.)*linkedin\\.com", "^https?://([a-z0-9-]+\\.)*twitter\\.com", "^https?://([a-z0-9-]+\\.)*x\\.com", + "^https?://([a-z0-9-]+\\.)*opencollective\\.com", "^https?://localhost(:\\d+)?(/|$)", ] diff --git a/package-lock.json b/package-lock.json index 6e1994f..fc0c6f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,38 +1,40 @@ { "name": "textrefs", - "version": "0.0.1", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "textrefs", - "version": "0.0.1", + "version": "0.1.0", "license": "AGPL-3.0-or-later", "dependencies": { - "@astrojs/starlight": "^0.39.2", - "astro": "^6.3.1", - "astro-mermaid": "^2.0.1", + "@astrojs/sitemap": "^3.7.3", + "@astrojs/starlight": "^0.41.4", + "astro": "^7.1.3", + "astro-mermaid": "^2.1.0", "mermaid": "^11.15.0", - "sharp": "^0.34.5", - "starlight-blog": "^0.26.1", + "sharp": "^0.35.3", + "spdx-license-ids": "^3.0.23", + "starlight-blog": "^0.28.0", "uuid": "^14.0.0", "yaml": "^2.9.0", - "zod": "^3.23.8" + "zod": "^4.4.3" }, "devDependencies": { "@astrojs/check": "^0.9.9", - "@commitlint/cli": "^21.0.1", + "@commitlint/cli": "^21.2.1", "@commitlint/config-conventional": "^21.0.1", "git-cliff": "^2.13.1", "husky": "^9.1.7", - "lint-staged": "^17.0.5", + "lint-staged": "^17.2.0", "pagefind": "^1.5.2", - "prettier": "^3.8.3", + "prettier": "^3.9.6", "prettier-plugin-astro": "^0.14.1", - "starlight-links-validator": "^0.24.0", - "starlight-llms-txt": "^0.10.0", - "starlight-openapi": "^0.25.3", - "tsx": "^4.19.2", + "starlight-links-validator": "^0.25.2", + "starlight-llms-txt": "^0.11.0", + "starlight-openapi": "^0.26.0", + "tsx": "^4.23.1", "typescript": "^6.0.3" } }, @@ -67,16 +69,16 @@ } }, "node_modules/@astrojs/check": { - "version": "0.9.9", - "resolved": "https://registry.npmjs.org/@astrojs/check/-/check-0.9.9.tgz", - "integrity": "sha512-A5UW8uIuErLWEoRQvzgXpO1gTjUFtK8r7nU2Z7GewAMxUb7bPvpk11qaKKgxqXlHJWlAvaaxy+Xg28A6bmQ1Tg==", + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/@astrojs/check/-/check-0.9.10.tgz", + "integrity": "sha512-zgx/UQMozdjOa3bOxjgeCFdtpE3c9rRX6xHwa+2QXvy8z8Akifu2AtubHyv/zzC2znO8dl8fFWL4K+Ba9kS8HQ==", "dev": true, "license": "MIT", "dependencies": { "@astrojs/language-server": "^2.16.7", "chokidar": "^4.0.3", "kleur": "^4.1.5", - "yargs": "^17.7.2" + "yargs": "^18.0.0" }, "bin": { "astro-check": "bin/astro-check.js" @@ -92,15 +94,203 @@ "dev": true, "license": "MIT" }, + "node_modules/@astrojs/compiler-binding": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding/-/compiler-binding-0.3.2.tgz", + "integrity": "sha512-8w/9CWmYrAJJ8N0SY3O43ws2BgxoW6u3QsD8u2mE140lMYAlwh+tlNoUeSBq22wVheFuiBbR212l6ixZ2IIgCQ==", + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@astrojs/compiler-binding-darwin-arm64": "0.3.2", + "@astrojs/compiler-binding-darwin-x64": "0.3.2", + "@astrojs/compiler-binding-linux-arm64-gnu": "0.3.2", + "@astrojs/compiler-binding-linux-arm64-musl": "0.3.2", + "@astrojs/compiler-binding-linux-x64-gnu": "0.3.2", + "@astrojs/compiler-binding-linux-x64-musl": "0.3.2", + "@astrojs/compiler-binding-wasm32-wasi": "0.3.2", + "@astrojs/compiler-binding-win32-arm64-msvc": "0.3.2", + "@astrojs/compiler-binding-win32-x64-msvc": "0.3.2" + } + }, + "node_modules/@astrojs/compiler-binding-darwin-arm64": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-arm64/-/compiler-binding-darwin-arm64-0.3.2.tgz", + "integrity": "sha512-MM8tn8CSimcfytaOla4b6acN8mKWiL/rlAA1fpT3/Wl7dNGSE4y8FjTN/zJVNnb63CsLWG5zZwCt01TXtDKh9g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-darwin-x64": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-x64/-/compiler-binding-darwin-x64-0.3.2.tgz", + "integrity": "sha512-2lXOlzf8xb7jLomRsf/aswh61/NnGusynB2OwFkK6k4pmOtpfXMYnG0PLfXrEvxXYj69NdCnmUYXtHDd+JOOag==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-linux-arm64-gnu": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-gnu/-/compiler-binding-linux-arm64-gnu-0.3.2.tgz", + "integrity": "sha512-BmU3kWj7qnLrd4vzm49zFEPJ5oFnn1tCT4Vt9hZbqdU5Cmb8GZl7fn6VFsnNfe7B18a2gIFtVzbLINtYl5kBjQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-linux-arm64-musl": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-musl/-/compiler-binding-linux-arm64-musl-0.3.2.tgz", + "integrity": "sha512-f0heT9ZZEseSu5bHCeb80eL2DH07ArE6U9xi1WT/PEusNjzPmEr3GJsjG1tRLo5VYUUYX7h3ScaqGmGrMOVGmw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-linux-x64-gnu": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-gnu/-/compiler-binding-linux-x64-gnu-0.3.2.tgz", + "integrity": "sha512-M8fOUt0itRpqiGyoEA/ij184s8O+hqbCz3+YozRusOOM3osgGljpDThhbKAJjqh82wOo6FioQ4w8PBvU1XMD5Q==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-linux-x64-musl": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-musl/-/compiler-binding-linux-x64-musl-0.3.2.tgz", + "integrity": "sha512-/Kebk8sO6HnLeSd691JkaAPfN7CqR9/KEXmWvyNPkaKNGmj8rTZ/lf2uXtnPu92Lan84UrKdIKVPy1fSo2encQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-wasm32-wasi": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-wasm32-wasi/-/compiler-binding-wasm32-wasi-0.3.2.tgz", + "integrity": "sha512-pUA6xbcOSB7DhfzIArB8BCAkFfAIqriiR7zl5zOStd6oU2G0kIKj+GUdGnyXyhfiv881Hffyk5tC0mR18sDjDw==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.2.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@astrojs/compiler-binding-win32-arm64-msvc": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-arm64-msvc/-/compiler-binding-win32-arm64-msvc-0.3.2.tgz", + "integrity": "sha512-ESruf+6Qkl1trHUFxI6GSf6t52j8yN2kCNSzMWdzt7V/T09tFHrYzrVaJQohb2C9bJUH76pNvX6Zb51+xCQc9Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-win32-x64-msvc": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-x64-msvc/-/compiler-binding-win32-x64-msvc-0.3.2.tgz", + "integrity": "sha512-wzzVrEbOwbsLWOdEbocskjMRx2aZPxJ7ZbmL+jnpBamFwmigm+2M/wzuM6JWncocgYwLic1csSpalBh96kQKXA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-rs": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-rs/-/compiler-rs-0.3.2.tgz", + "integrity": "sha512-xlx/T7JovIKduu4ucbTQUxQ5+Q8wxkHxhLjnZk3VlJbhbQ9RLvvuDk1p2YYFYFQ5y14dVm3FGGO4isQXa4F+Tg==", + "license": "MIT", + "dependencies": { + "@astrojs/compiler-binding": "0.3.2" + }, + "engines": { + "node": ">=22.12.0" + } + }, "node_modules/@astrojs/internal-helpers": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.10.0.tgz", - "integrity": "sha512-Ry2R3VPeIN4uPCSA4xQc+e+vsJXkalKpEbDc07hV+a/o5Bs2N/s/uDcPJH/05L19DKh9tAy7e6JM3YZ6Cxfezw==", + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.10.2.tgz", + "integrity": "sha512-yt7fMgPYqSM4Tmr+taTW6Per+hjJ8Pk6lA1PAcDyqzOt8HzJ6Kje5WzCxA2Sd+9wsUW7uhkLeoTMK0cXPwH9rQ==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", - "js-yaml": "^4.1.1", + "js-yaml": "^4.3.0", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", @@ -109,9 +299,9 @@ } }, "node_modules/@astrojs/language-server": { - "version": "2.16.10", - "resolved": "https://registry.npmjs.org/@astrojs/language-server/-/language-server-2.16.10.tgz", - "integrity": "sha512-87VQ/5GSdHlRnUA+hGuerYyIGAj+9RbZmATyuKLEUePinUXhQ5YkRnRrHhOD9sSi5JOErLjrLkHnfZFEvGrV8w==", + "version": "2.16.14", + "resolved": "https://registry.npmjs.org/@astrojs/language-server/-/language-server-2.16.14.tgz", + "integrity": "sha512-YPXkBu6N4d1sT09pvBmIDGZay+1MemV551FSgdEM3aZRDzbkxd2H7Cvf8MsVJLVB1mIEyTf1XbMN/30gp7s46w==", "dev": true, "license": "MIT", "dependencies": { @@ -124,13 +314,13 @@ "@volar/language-service": "~2.4.28", "muggle-string": "^0.4.1", "tinyglobby": "^0.2.16", - "volar-service-css": "0.0.70", - "volar-service-emmet": "0.0.70", - "volar-service-html": "0.0.70", - "volar-service-prettier": "0.0.70", - "volar-service-typescript": "0.0.70", - "volar-service-typescript-twoslash-queries": "0.0.70", - "volar-service-yaml": "0.0.70", + "volar-service-css": "0.0.71", + "volar-service-emmet": "0.0.71", + "volar-service-html": "0.0.71", + "volar-service-prettier": "0.0.71", + "volar-service-typescript": "0.0.71", + "volar-service-typescript-twoslash-queries": "0.0.71", + "volar-service-yaml": "0.0.71", "vscode-html-languageservice": "^5.6.2", "vscode-uri": "^3.1.0" }, @@ -151,12 +341,12 @@ } }, "node_modules/@astrojs/markdown-remark": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-7.2.0.tgz", - "integrity": "sha512-+YxmVQu1Bd+MFfSzjq1rOJvD9+nIOJzz5YIIhdIH01RrxRkKbyKoEgyIqP3yv51MhzMDgd79QaPv+kCVPT8vHw==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-7.2.2.tgz", + "integrity": "sha512-FGfmK84zSNcrsBd0dl1gXE9JvZYElp8EXQa2jpHVAxG4deGKAp43wspxFupjADJX7MSsMRHwYCnfT6EyVmgeFQ==", "license": "MIT", "dependencies": { - "@astrojs/internal-helpers": "0.10.0", + "@astrojs/internal-helpers": "0.10.2", "@astrojs/prism": "4.0.2", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", @@ -175,13 +365,27 @@ "vfile": "^6.0.3" } }, + "node_modules/@astrojs/markdown-satteri": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-satteri/-/markdown-satteri-0.3.5.tgz", + "integrity": "sha512-CvWVEFAbay7YO+i9SaqDJubipA5ckiVB89QWoMJ5XC0m5CtFg8JwZ7Kau6X9sYY7FZURH0w2l03ISH2jOS/RDQ==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.10.2", + "@astrojs/prism": "4.0.2", + "github-slugger": "^2.0.0", + "hast-util-from-html": "^2.0.3", + "satteri": "^0.9.1" + } + }, "node_modules/@astrojs/mdx": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-5.0.6.tgz", - "integrity": "sha512-4dKe0ZMmqujofPNDHahzClkwinn9f8jHPcaXcgdGvPAlboD2mjzkUCofli2cBnxYAkdfhC6d50gBJ8i/cH8gHw==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-7.0.5.tgz", + "integrity": "sha512-wEM/HH1RiEntyPVagdiF+yArzfcYLKBB0C1RZspVidKZ97rRMbaqP1Nbl/GR0sJs8zwaceqxRymw8aOKKJRdYw==", "license": "MIT", "dependencies": { - "@astrojs/markdown-remark": "7.1.2", + "@astrojs/internal-helpers": "0.10.2", + "@astrojs/markdown-remark": "7.2.2", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.16.0", "es-module-lexer": "^2.0.0", @@ -199,45 +403,13 @@ "node": ">=22.12.0" }, "peerDependencies": { - "astro": "^6.0.0" - } - }, - "node_modules/@astrojs/mdx/node_modules/@astrojs/internal-helpers": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.9.1.tgz", - "integrity": "sha512-1pWuARqYom/TzuU3+0ZugsTrKlUydWKuULmDqSMTuonY+9IRDUEGKX/8PXQ1nBxRq3w85uGtd9q9SXfqEldMIQ==", - "license": "MIT", - "dependencies": { - "picomatch": "^4.0.4" - } - }, - "node_modules/@astrojs/mdx/node_modules/@astrojs/markdown-remark": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-7.1.2.tgz", - "integrity": "sha512-caXZ4Dc2St2dW8luEg22GlP0gupLdztCTQE4EzZOxW1pqWXz9mbeJEuHUkgDYcKWW8tjIHkydYDhWLVoxJ327Q==", - "license": "MIT", - "dependencies": { - "@astrojs/internal-helpers": "0.9.1", - "@astrojs/prism": "4.0.2", - "github-slugger": "^2.0.0", - "hast-util-from-html": "^2.0.3", - "hast-util-to-text": "^4.0.2", - "js-yaml": "^4.1.1", - "mdast-util-definitions": "^6.0.0", - "rehype-raw": "^7.0.0", - "rehype-stringify": "^10.0.1", - "remark-gfm": "^4.0.1", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.1.2", - "remark-smartypants": "^3.0.2", - "retext-smartypants": "^6.2.0", - "shiki": "^4.0.0", - "smol-toml": "^1.6.0", - "unified": "^11.0.5", - "unist-util-remove-position": "^5.0.0", - "unist-util-visit": "^5.1.0", - "unist-util-visit-parents": "^6.0.2", - "vfile": "^6.0.3" + "@astrojs/markdown-satteri": "^0.3.1", + "astro": "^7.0.0" + }, + "peerDependenciesMeta": { + "@astrojs/markdown-satteri": { + "optional": true + } } }, "node_modules/@astrojs/prism": { @@ -253,9 +425,9 @@ } }, "node_modules/@astrojs/rss": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@astrojs/rss/-/rss-4.0.18.tgz", - "integrity": "sha512-wc5DwKlbTEdgVAWnHy8krFTeQ42t1v/DJqeq5HtulYK3FYHE4krtRGjoyhS3eXXgfdV6Raoz2RU3wrMTFAitRg==", + "version": "4.0.19", + "resolved": "https://registry.npmjs.org/@astrojs/rss/-/rss-4.0.19.tgz", + "integrity": "sha512-e+z5wYeYtffQdHQO8c2tkSd2JEBdAuRXJV4ZEU5IxkYeE6e39woDd7nw1PH1Kk2tEYNCYuKdylnnbhGmt61awA==", "license": "MIT", "dependencies": { "fast-xml-parser": "^5.5.7", @@ -263,15 +435,6 @@ "zod": "^4.3.6" } }, - "node_modules/@astrojs/rss/node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "node_modules/@astrojs/sitemap": { "version": "3.7.3", "resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.7.3.tgz", @@ -283,29 +446,20 @@ "zod": "^4.3.6" } }, - "node_modules/@astrojs/sitemap/node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "node_modules/@astrojs/starlight": { - "version": "0.39.3", - "resolved": "https://registry.npmjs.org/@astrojs/starlight/-/starlight-0.39.3.tgz", - "integrity": "sha512-uvAweA2DwhmLgFVfBT9NqG38Ey14k1ck3+y78XNJbceT1pMdzxCCX69RoBajb1QzTJviufsXzSc1xswgRxJfig==", + "version": "0.41.7", + "resolved": "https://registry.npmjs.org/@astrojs/starlight/-/starlight-0.41.7.tgz", + "integrity": "sha512-579VJuZgo20UpNQPm9EIez5W3DFSrD16uiV2YX6rUlpLtjgKSdnc69TxVTZXn4AtI2B731TI2qhW1O3K+vwtrQ==", "license": "MIT", "dependencies": { - "@astrojs/markdown-remark": "^7.1.1", - "@astrojs/mdx": "^5.0.4", - "@astrojs/sitemap": "^3.7.2", + "@astrojs/markdown-satteri": "^0.3.5", + "@astrojs/mdx": "^7.0.5", + "@astrojs/sitemap": "^3.7.3", "@pagefind/default-ui": "^1.3.0", "@types/hast": "^3.0.4", "@types/js-yaml": "^4.0.9", "@types/mdast": "^4.0.4", - "astro-expressive-code": "^0.42.0", + "astro-expressive-code": "^0.44.0", "bcp-47": "^2.1.0", "hast-util-from-html": "^2.0.3", "hast-util-select": "^6.0.4", @@ -322,26 +476,32 @@ "rehype": "^13.0.2", "rehype-format": "^5.0.1", "remark-directive": "^4.0.0", + "satteri": "^0.9.1", "ultrahtml": "^1.6.0", "unified": "^11.0.5", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { - "astro": "^6.0.0" + "@astrojs/markdown-remark": "^7.2.0", + "astro": "^7.0.2" + }, + "peerDependenciesMeta": { + "@astrojs/markdown-remark": { + "optional": true + } } }, "node_modules/@astrojs/telemetry": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.2.tgz", - "integrity": "sha512-j8DNruA8ors99Al39RYZPJK4DC1bKkoNm93mAMuBhY9TCNC4R8n1q7ovFnJ5qhGh5Lsh7pa1gpQVpYpsJPeTHQ==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.3.tgz", + "integrity": "sha512-C1TLn5sPJr0x4vk56piHWKbnqlEB8BKyte5Y45V02U+D7BGO5eMqZDH5aPjnkXQWJggvmsTXxH03QMZ9NgWLzQ==", "license": "MIT", "dependencies": { "ci-info": "^4.4.0", "dset": "^3.1.4", "is-docker": "^4.0.0", - "is-wsl": "^3.1.1", - "which-pm-runs": "^1.1.0" + "package-manager-detector": "^1.6.0" }, "engines": { "node": "18.20.8 || ^20.3.0 || >=22.0.0" @@ -391,12 +551,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.29.7" + "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -416,9 +576,9 @@ } }, "node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.29.7", @@ -434,165 +594,262 @@ "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", "license": "MIT" }, - "node_modules/@capsizecss/unpack": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-4.0.0.tgz", - "integrity": "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA==", + "node_modules/@bruits/satteri-darwin-arm64": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-arm64/-/satteri-darwin-arm64-0.9.5.tgz", + "integrity": "sha512-iw4nZgx9v30lWo/MTngQqi1pI78KI0DnkSm+lVJGYdmPLgAyDNJigVhpG42/Iq55A6c1Ll8q66ljyyRiQUxwow==", + "cpu": [ + "arm64" + ], "license": "MIT", - "dependencies": { - "fontkitten": "^1.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@chevrotain/types": { - "version": "11.1.2", - "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", - "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", - "license": "Apache-2.0" + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@clack/core": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.0.tgz", - "integrity": "sha512-7Wctjq6f7c1CPz8sPpkwUnz8yRgVANkpNupb81q432FjcJg4l+Sw7XANdNSdWfAKq0IHI0JTcUeK5dxs/HrGPw==", + "node_modules/@bruits/satteri-darwin-x64": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-x64/-/satteri-darwin-x64-0.9.5.tgz", + "integrity": "sha512-6T26Z5Kf3cFW2PSlk9p7zT7yVxvuBSiJvYyz9u8KjYwMTqZyIDOj2wDyNpxKV4+6yUVG7rddq2QwvG/8LJA2+Q==", + "cpu": [ + "x64" + ], "license": "MIT", - "dependencies": { - "fast-wrap-ansi": "^0.2.0", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 20.12.0" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@clack/prompts": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.5.0.tgz", - "integrity": "sha512-wKh+wTjmrUoUdkZg8KpJO5X+p9PWV+KE9mePseq9UYWkukgTKsGS47RRL2HstwVcvDQH+PenrPJWII8+MfiiyA==", + "node_modules/@bruits/satteri-linux-arm64-gnu": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-gnu/-/satteri-linux-arm64-gnu-0.9.5.tgz", + "integrity": "sha512-u51id17uJwNEMK9nBlICsq6U31c+XVqQueVBkwRIzZG+gMpS8TOJctt5h5Wz33Z8xnMdTd+adtACVz0yHgGuOA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], "license": "MIT", - "dependencies": { - "@clack/core": "1.4.0", - "fast-string-width": "^3.0.2", - "fast-wrap-ansi": "^0.2.0", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 20.12.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@commitlint/cli": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-21.0.2.tgz", - "integrity": "sha512-YMmfLbqBg+ZRvvmPhc+cilSQFrh/AgzVgCT1U/OifmUZEwPbvCtA8rN//YNaF9d5eoZphxVMGYtmwA2QgQORgg==", - "dev": true, + "node_modules/@bruits/satteri-linux-arm64-musl": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-musl/-/satteri-linux-arm64-musl-0.9.5.tgz", + "integrity": "sha512-v39HxiwGC5Rqm01HksP6+5Y+xKLPlsuVFgIgpEAo+SiQ22c+mJVhS3u7Z6ePAKdhL5NJoK1xq70kLz3L13AhpQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@bruits/satteri-linux-x64-gnu": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-gnu/-/satteri-linux-x64-gnu-0.9.5.tgz", + "integrity": "sha512-F3uO8uFp3pAP5ZGXttwvh57GS7s0lL953tnNdyI2gRyP4kOOkp6pyGojNJzCjkDvWI2Cvb9iNrKok3aqQPauAw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@bruits/satteri-linux-x64-musl": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-musl/-/satteri-linux-x64-musl-0.9.5.tgz", + "integrity": "sha512-bicEqglLlz++mWyADaZoP0JY20s4vDfLjaPYgQqC+NI4zZLTOOg1T4GB8aqtc822Pqji8SQBmSrTb7CrP8i08Q==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@bruits/satteri-wasm32-wasi": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-wasm32-wasi/-/satteri-wasm32-wasi-0.9.5.tgz", + "integrity": "sha512-zauAuMwfPnKPUkd4AFixRFpXdgKwP2mKgxrIIo2gJzW0/ZneF9dbHnLkojSpaBnCCp7VUL1hIi5WWZvB1CqmAQ==", + "cpu": [ + "wasm32" + ], "license": "MIT", + "optional": true, "dependencies": { - "@commitlint/format": "^21.0.1", - "@commitlint/lint": "^21.0.2", - "@commitlint/load": "^21.0.2", - "@commitlint/read": "^21.0.2", - "@commitlint/types": "^21.0.1", - "tinyexec": "^1.0.0", - "yargs": "^18.0.0" - }, - "bin": { - "commitlint": "cli.js" + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" }, "engines": { - "node": ">=22.12.0" + "node": ">=14.0.0" } }, - "node_modules/@commitlint/cli/node_modules/cliui": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", - "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", - "dev": true, - "license": "ISC", + "node_modules/@bruits/satteri-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "license": "MIT", + "optional": true, "dependencies": { - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=20" + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" } }, - "node_modules/@commitlint/cli/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, + "node_modules/@bruits/satteri-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "license": "MIT", + "optional": true, "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" + "tslib": "^2.4.0" + } + }, + "node_modules/@bruits/satteri-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@bruits/satteri-win32-arm64-msvc": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-arm64-msvc/-/satteri-win32-arm64-msvc-0.9.5.tgz", + "integrity": "sha512-SrfE7NEsgZjBvU3c+RR6oQRu0ToXY5uVJEbieXEF0YTctIV2zAVlbaMjWLts074QCgh3a+XHWkR/lWh2VH2LUg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@bruits/satteri-win32-x64-msvc": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-x64-msvc/-/satteri-win32-x64-msvc-0.9.5.tgz", + "integrity": "sha512-5Kw9ZAtTGS8WHizyn+CJhjjfIQrw+7jcZodpmpXJjefnO15M8UexIi6JR2E5thyvsmHyhL6ZDDMUNR4bKJPd4g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@capsizecss/unpack": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-4.0.1.tgz", + "integrity": "sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.3" }, "engines": { "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@commitlint/cli/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, + "node_modules/@chevrotain/types": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", + "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", + "license": "Apache-2.0" + }, + "node_modules/@clack/core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.3.tgz", + "integrity": "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==", "license": "MIT", "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" }, "engines": { - "node": ">=18" + "node": ">= 20.12.0" + } + }, + "node_modules/@clack/prompts": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.7.0.tgz", + "integrity": "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==", + "license": "MIT", + "dependencies": { + "@clack/core": "1.4.3", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "engines": { + "node": ">= 20.12.0" } }, - "node_modules/@commitlint/cli/node_modules/yargs": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", - "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "node_modules/@commitlint/cli": { + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-21.2.1.tgz", + "integrity": "sha512-blsZGe29hJ72VGEFVl72IVYX+1vsfINpjA9yWQA6i7OKD/McGEOXg08sKIRKjFk4JvzhV/9n0l3i6NooPLTNfg==", "dev": true, "license": "MIT", "dependencies": { - "cliui": "^9.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "string-width": "^7.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^22.0.0" + "@commitlint/config-conventional": "^21.2.0", + "@commitlint/format": "^21.2.0", + "@commitlint/lint": "^21.2.0", + "@commitlint/load": "^21.2.0", + "@commitlint/read": "^21.2.1", + "@commitlint/types": "^21.2.0", + "tinyexec": "^1.0.0", + "yargs": "^18.0.0" + }, + "bin": { + "commitlint": "cli.js" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" + "node": ">=22.12.0" } }, "node_modules/@commitlint/config-conventional": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-21.0.2.tgz", - "integrity": "sha512-P/ZRhryQmkj0Z0dY9FOoRwe3xkwJyyAdtXwt01NT2kuZttcG2CNYp1q5Ci3u+nDT2jcbJRw2kt13Czl1qKNPfg==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-21.2.0.tgz", + "integrity": "sha512-Qf8WRDVcyVd14if6VTWenebxFbKnVnbzPUJjlzjkyJGeHK2xCGd63Dr1XZzj0plXKQb9P0BfOxoc1HVeCo2BWQ==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^21.0.1", - "conventional-changelog-conventionalcommits": "^9.2.0" + "@commitlint/types": "^21.2.0", + "conventional-changelog-conventionalcommits": "^10.0.0" }, "engines": { "node": ">=22.12.0" } }, "node_modules/@commitlint/config-validator": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-21.0.1.tgz", - "integrity": "sha512-Zd2UFdndeMMaW2O96HK0tdfT4gOImUvidMpAd/pws2zZ4m1nrAZ/9b/v2JYuE8fs86GpXv9F7LNaIuCIWhY+pA==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-21.2.0.tgz", + "integrity": "sha512-t7AzNHAKeIdo/3NRGwzpufKHsKkPHmFs/56N2Fnsh0/r0rGtnQzTxk6vnFgjaGr4hdSQKNB50/KAhR9Yk4LJKA==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^21.0.1", + "@commitlint/types": "^21.2.0", "ajv": "^8.11.0" }, "engines": { @@ -600,13 +857,13 @@ } }, "node_modules/@commitlint/ensure": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-21.0.1.tgz", - "integrity": "sha512-jJ1037967wU7YN/xkv+iRlOBlmaOXPhPO5KQSqya6GyXzBlwuLzELBFao16DVg9dZyqmNrhewzwZ3SAibetHBQ==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-21.2.0.tgz", + "integrity": "sha512-76IF9vDNS13lAzEEik9eKwzt8f9hYhWiwVXZ2AnyLCz5/f511FsEQ3pw1X3/zSQpdRLQU7i5qDMVKyXi1GWjSg==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^21.0.1", + "@commitlint/types": "^21.2.0", "es-toolkit": "^1.46.0" }, "engines": { @@ -624,13 +881,13 @@ } }, "node_modules/@commitlint/format": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-21.0.1.tgz", - "integrity": "sha512-ksmG2+cHGtuDPQQbhBbC4unwm444+6TiPw0d1bKf67hntgZqZ8E0g1MuYKUuyT5IH4IMmXZhKq22/Z3jBvtQIw==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-21.2.0.tgz", + "integrity": "sha512-c4q64xaav2U83t7k7RyzJerBZurPer7FxUOY0RL5L/6CZijZ7K+s6HIBGIghj0ey1P2+seRX0J9XQYtDued6tg==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^21.0.1", + "@commitlint/types": "^21.2.0", "picocolors": "^1.1.1" }, "engines": { @@ -638,13 +895,13 @@ } }, "node_modules/@commitlint/is-ignored": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-21.0.2.tgz", - "integrity": "sha512-H5z4t8PC9tUsmZ/o+EptM3Nq8sTFtskAShdcqxCoyzklW5eaVT5xbrDAET2uypzir9Vsj4ZZmBtyKjYe2XqgeQ==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-21.2.0.tgz", + "integrity": "sha512-4/eB0vBN7L88O/oC4ajAEqi7j2ZfNgxl/+11RfAV9YosejZgDXhY2C9VcHnHJhOzPLoSy5P3Mg/46kqeyJfXKw==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^21.0.1", + "@commitlint/types": "^21.2.0", "semver": "^7.6.0" }, "engines": { @@ -652,32 +909,32 @@ } }, "node_modules/@commitlint/lint": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-21.0.2.tgz", - "integrity": "sha512-PnUmLYGeGLfW8oVatR9KpNxSHYAnJOEWlMZzfdeFOUq6WUrFx1fGQaWCWJqMoIll/xPM+GdfJV+tKHZVHhl0Fg==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-21.2.0.tgz", + "integrity": "sha512-ceO5dp9pLjEZ6y6qbq/uXWXDPykqqlTsyzoQ0NzecpisSJhK3kTy9qzQoPeJuWG/IMNdV1lO0RgmzqoAlSi1uw==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/is-ignored": "^21.0.2", - "@commitlint/parse": "^21.0.2", - "@commitlint/rules": "^21.0.2", - "@commitlint/types": "^21.0.1" + "@commitlint/is-ignored": "^21.2.0", + "@commitlint/parse": "^21.2.0", + "@commitlint/rules": "^21.2.0", + "@commitlint/types": "^21.2.0" }, "engines": { "node": ">=22.12.0" } }, "node_modules/@commitlint/load": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-21.0.2.tgz", - "integrity": "sha512-lwUE70hN0/qE/ZRROhbaX65ly/FF12DrqfReLCESo37M0OQCFAf2jRS+2tSCSORq+bm4Kdju7qNDj46uc1QzTA==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-21.2.0.tgz", + "integrity": "sha512-RjlzWQqruRwIenJEfZtq7kG97co97nKoHpflE5YnF61tDLXxHPrdWImgzw6VL6MlFyaOcVlk74eBV8ZQmc3oIA==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/config-validator": "^21.0.1", + "@commitlint/config-validator": "^21.2.0", "@commitlint/execute-rule": "^21.0.1", - "@commitlint/resolve-extends": "^21.0.1", - "@commitlint/types": "^21.0.1", + "@commitlint/resolve-extends": "^21.2.0", + "@commitlint/types": "^21.2.0", "cosmiconfig": "^9.0.1", "cosmiconfig-typescript-loader": "^6.1.0", "es-toolkit": "^1.46.0", @@ -689,9 +946,9 @@ } }, "node_modules/@commitlint/message": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-21.0.2.tgz", - "integrity": "sha512-5n4aqHGD/FNnom/D5L8i7cYtV+xjuXcBL832C3w9VglEsZzIsoHpJsvxzJ7cgiOsOdc/2jU4t5+7qMHh7GBX3g==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-21.2.0.tgz", + "integrity": "sha512-YxGoiXD/HXNXLJPrQwE5poXa+XH0CBEm+mdvbHQP0g6MV/dmJyUFCzPNzZbxL93GvZ70TmtTK0Z0/IBpAqHv8g==", "dev": true, "license": "MIT", "engines": { @@ -699,30 +956,30 @@ } }, "node_modules/@commitlint/parse": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-21.0.2.tgz", - "integrity": "sha512-QVZJhGHTm+oiuWyEKOCTQ0ZM3mfJ0eGWFeHuj7WzSKEth+UukcCHac9GD8pgdFlg/qGkFWOtyaNd1T8REgagaw==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-21.2.0.tgz", + "integrity": "sha512-QHWxG4d0PLTF634/AdyZ0MQS+CLn5YOuJlCFhMMlSGKFxzYGUetkHBj18xgBD+6fVzUrA2lrCdi/vlS2f/oYXg==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^21.0.1", - "conventional-changelog-angular": "^8.2.0", - "conventional-commits-parser": "^6.3.0" + "@commitlint/types": "^21.2.0", + "conventional-changelog-angular": "^9.0.0", + "conventional-commits-parser": "^7.0.0" }, "engines": { "node": ">=22.12.0" } }, "node_modules/@commitlint/read": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-21.0.2.tgz", - "integrity": "sha512-BtsrnLVycSSKf4Q0gMch4giCj5NNlmcbhc8ra5vONgGtP2IjRDo33bEFtr5Pm+2N+5fXGWb2MksWPrspPfdhdw==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-21.2.1.tgz", + "integrity": "sha512-hUW7EJQnNTL0vPOmVMNK4CrnrNBN0nN+JJHReFkdHO5y4iyHeEmTBwuC15OCqUTjxWo7idnH1LftfpWVIaPWIA==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/top-level": "^21.0.2", - "@commitlint/types": "^21.0.1", - "git-raw-commits": "^5.0.0", + "@commitlint/top-level": "^21.2.0", + "@commitlint/types": "^21.2.0", + "@conventional-changelog/git-client": "^3.0.0", "tinyexec": "^1.0.0" }, "engines": { @@ -730,14 +987,14 @@ } }, "node_modules/@commitlint/resolve-extends": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-21.0.1.tgz", - "integrity": "sha512-0DhjYWL6uYrY16Efa032fYk3woGJDU4AGWiG1XXltT9AMUNYKyb5cIZU2ivbaMZ3+kKFqUjikD2cjh66Sbh/Sg==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-21.2.0.tgz", + "integrity": "sha512-4O/1j51+79Wth9s/MGxt/5gs0XYLDgNlYpltQfhAvLE0itusLKs9zruxbiNg1oOkmkb9L9L4USYGjEj7n87NxA==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/config-validator": "^21.0.1", - "@commitlint/types": "^21.0.1", + "@commitlint/config-validator": "^21.2.0", + "@commitlint/types": "^21.2.0", "es-toolkit": "^1.46.0", "global-directory": "^5.0.0", "resolve-from": "^5.0.0" @@ -747,16 +1004,16 @@ } }, "node_modules/@commitlint/rules": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-21.0.2.tgz", - "integrity": "sha512-k6tQ69Td7t2qUSIbik8D3TL1q3ZJpkEbV+yLogDzCRAdOxJm4ndhtBNREsLA1/puRfWvzS9eioF2w43WT+hHgQ==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-21.2.0.tgz", + "integrity": "sha512-C2yXMNpiB8ETZKfx5JD8+ExgF8vTU1VQMKPSUUYwqKpw9oJWQBrlXBpdU038mj2WPjof7o9UzFpmTyBeGMZwZg==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/ensure": "^21.0.1", - "@commitlint/message": "^21.0.2", + "@commitlint/ensure": "^21.2.0", + "@commitlint/message": "^21.2.0", "@commitlint/to-lines": "^21.0.1", - "@commitlint/types": "^21.0.1" + "@commitlint/types": "^21.2.0" }, "engines": { "node": ">=22.12.0" @@ -773,9 +1030,9 @@ } }, "node_modules/@commitlint/top-level": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-21.0.2.tgz", - "integrity": "sha512-s9KKM+e+mXgFeIh4n7KmOGAVT3mkJ3Fp1bBYHIK5pjeUwlEMzp/tZfb5u0Poa680AsQTXMEMRxZi1vQ9m2X5ug==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-21.2.0.tgz", + "integrity": "sha512-Y5gmQ+KxzqCrBFJfLvFEPvvwD3LDiNZoTT2yeFBm96M8qhmqSzQc5DvX3rheAaAMjyIvMXOCLS/mWfdpONsjyQ==", "dev": true, "license": "MIT", "dependencies": { @@ -786,13 +1043,13 @@ } }, "node_modules/@commitlint/types": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-21.0.1.tgz", - "integrity": "sha512-4u7w8jcoCUFWhjWnASYzZHAP34OqOtuFBN87nQmFvqda03YU0T6z+yB4w0gSAMpekiRqqGk5rt+qSlW+a2vSEg==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-21.2.0.tgz", + "integrity": "sha512-7zVFCDB2reMvJH5dmbKnOQPjZEvjdJTH8jc0U/PIPU1r3/+vf5pD1HlfitV2MWsWXrvu7u39iY1lyLUPOaN0Gw==", "dev": true, "license": "MIT", "dependencies": { - "conventional-commits-parser": "^6.3.0", + "conventional-commits-parser": "^7.0.0", "picocolors": "^1.1.1" }, "engines": { @@ -800,22 +1057,22 @@ } }, "node_modules/@conventional-changelog/git-client": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@conventional-changelog/git-client/-/git-client-2.7.0.tgz", - "integrity": "sha512-j7A8/LBEQ+3rugMzPXoKYzyUPpw/0CBQCyvtTR7Lmu4olG4yRC/Tfkq79Mr3yuPs0SUitlO2HwGP3gitMJnRFw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@conventional-changelog/git-client/-/git-client-3.1.2.tgz", + "integrity": "sha512-jZqwnJwf7nboIlAcw/mkOjVa6DexCcUOgT2oOQgkoi3z9vR8tGFkcMy2BFcYwjhL9sYcDDXkRQDayiDieCoW7A==", "dev": true, "license": "MIT", "dependencies": { - "@simple-libs/child-process-utils": "^1.0.0", - "@simple-libs/stream-utils": "^1.2.0", + "@simple-libs/child-process-utils": "^2.0.0", + "@simple-libs/stream-utils": "^2.0.0", "semver": "^7.5.2" }, "engines": { - "node": ">=18" + "node": ">=22" }, "peerDependencies": { - "conventional-commits-filter": "^5.0.0", - "conventional-commits-parser": "^6.4.0" + "conventional-commits-filter": "^6.0.1", + "conventional-commits-parser": "^7.1.2" }, "peerDependenciesMeta": { "conventional-commits-filter": { @@ -826,6 +1083,16 @@ } } }, + "node_modules/@conventional-changelog/template": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@conventional-changelog/template/-/template-1.3.0.tgz", + "integrity": "sha512-GsCw/qu92GI0EX6s7fxUi/SG1lmFjG9XZivxxEDZXqztuQKCn5o5wKdz4v005zci0Md7EZzgAwmQLoIEDZoaow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + } + }, "node_modules/@ctrl/tinycolor": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", @@ -897,20 +1164,43 @@ "dev": true, "license": "MIT" }, + "node_modules/@emnapi/core": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", + "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.3", + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" } }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", + "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", "cpu": [ "ppc64" ], @@ -924,9 +1214,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", "cpu": [ "arm" ], @@ -940,9 +1230,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", "cpu": [ "arm64" ], @@ -956,9 +1246,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", "cpu": [ "x64" ], @@ -972,9 +1262,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", "cpu": [ "arm64" ], @@ -988,9 +1278,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", "cpu": [ "x64" ], @@ -1004,9 +1294,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", "cpu": [ "arm64" ], @@ -1020,9 +1310,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", "cpu": [ "x64" ], @@ -1036,9 +1326,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", "cpu": [ "arm" ], @@ -1052,9 +1342,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", "cpu": [ "arm64" ], @@ -1068,9 +1358,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", "cpu": [ "ia32" ], @@ -1084,9 +1374,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", "cpu": [ "loong64" ], @@ -1100,9 +1390,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", "cpu": [ "mips64el" ], @@ -1116,9 +1406,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", "cpu": [ "ppc64" ], @@ -1132,9 +1422,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", "cpu": [ "riscv64" ], @@ -1148,9 +1438,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", "cpu": [ "s390x" ], @@ -1164,9 +1454,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", "cpu": [ "x64" ], @@ -1180,9 +1470,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", "cpu": [ "arm64" ], @@ -1196,9 +1486,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", "cpu": [ "x64" ], @@ -1212,9 +1502,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", "cpu": [ "arm64" ], @@ -1228,9 +1518,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", "cpu": [ "x64" ], @@ -1244,9 +1534,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", "cpu": [ "arm64" ], @@ -1260,9 +1550,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", "cpu": [ "x64" ], @@ -1276,9 +1566,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", "cpu": [ "arm64" ], @@ -1292,9 +1582,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", "cpu": [ "ia32" ], @@ -1308,9 +1598,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", "cpu": [ "x64" ], @@ -1324,9 +1614,9 @@ } }, "node_modules/@expressive-code/core": { - "version": "0.42.0", - "resolved": "https://registry.npmjs.org/@expressive-code/core/-/core-0.42.0.tgz", - "integrity": "sha512-MN11+9nfmaC7sYu2BZJXAXqwkBRt8t1xTSqP+Ti1NfTEskgl6xUnzDxoaiQkg0BMzpglA0pys4dpDKquP/cyIw==", + "version": "0.44.1", + "resolved": "https://registry.npmjs.org/@expressive-code/core/-/core-0.44.1.tgz", + "integrity": "sha512-3dDo9N8D7hYrLNNMMWFovg3+aDUtnQm7c7z0GZc1c0LEFVBc0Q6lKG+tVT28gDadOvsgOANfCn35fgpe97Pmgg==", "license": "MIT", "dependencies": { "@ctrl/tinycolor": "^4.0.4", @@ -1341,31 +1631,31 @@ } }, "node_modules/@expressive-code/plugin-frames": { - "version": "0.42.0", - "resolved": "https://registry.npmjs.org/@expressive-code/plugin-frames/-/plugin-frames-0.42.0.tgz", - "integrity": "sha512-XtkPm+941Uta7Y+81Acv+OA/20F1NJmJhCX6UYGKpqEIGqplNh3PTOhcURp6tcruhlzJcWcvpWy6Oigz3SrjqA==", + "version": "0.44.1", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-frames/-/plugin-frames-0.44.1.tgz", + "integrity": "sha512-HC/bdRao9225ApcgO/e3jn8ZOhldKO7ob1O/Tcipvtv7Vb5nMphZhMtD9uuywpvxkPYBHJi3504WhrKg05Dwqg==", "license": "MIT", "dependencies": { - "@expressive-code/core": "^0.42.0" + "@expressive-code/core": "^0.44.1" } }, "node_modules/@expressive-code/plugin-shiki": { - "version": "0.42.0", - "resolved": "https://registry.npmjs.org/@expressive-code/plugin-shiki/-/plugin-shiki-0.42.0.tgz", - "integrity": "sha512-PMKey/kLmewttAHQezL+Y5Fx3vVssfDi3+FJOYQQS2mXP3tQspFELtKKAfsXfmSXdToZYgwoO69HJndqfE+09g==", + "version": "0.44.1", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-shiki/-/plugin-shiki-0.44.1.tgz", + "integrity": "sha512-YApiZt3buUzBwL5tqj8G+sYC5NjMjRCHgQwr9bmGl69rtcHy6fE9dooWUeKYB978fJT2BuxT5FeHcF47rA3SEg==", "license": "MIT", "dependencies": { - "@expressive-code/core": "^0.42.0", + "@expressive-code/core": "^0.44.1", "shiki": "^4.0.2" } }, "node_modules/@expressive-code/plugin-text-markers": { - "version": "0.42.0", - "resolved": "https://registry.npmjs.org/@expressive-code/plugin-text-markers/-/plugin-text-markers-0.42.0.tgz", - "integrity": "sha512-l59lUx8fq1v5g6SpmbDjiU0+7IdfbiWnAyRmtTVSpfhyq+nZMN4UcmYyu2b9Mynhzt7Gr+O+cXyEPDNb2AVWVQ==", + "version": "0.44.1", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-text-markers/-/plugin-text-markers-0.44.1.tgz", + "integrity": "sha512-B3BsJoJ8CFMlcIX9f+X9tcI3C4zPDO601+YuLi9GheSTNro7ZfqSjLptMQKBHOWZvxnAtY5zvIX7iO/qtBhNBg==", "license": "MIT", "dependencies": { - "@expressive-code/core": "^0.42.0" + "@expressive-code/core": "^0.44.1" } }, "node_modules/@humanwhocodes/momoa": { @@ -1385,9 +1675,9 @@ "license": "MIT" }, "node_modules/@iconify/utils": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.3.tgz", - "integrity": "sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.4.tgz", + "integrity": "sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==", "license": "MIT", "dependencies": { "@antfu/install-pkg": "^1.1.0", @@ -1405,9 +1695,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", + "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", "cpu": [ "arm64" ], @@ -1417,19 +1707,19 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" + "@img/sharp-libvips-darwin-arm64": "1.3.2" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", + "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", "cpu": [ "x64" ], @@ -1439,19 +1729,38 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" + "@img/sharp-libvips-darwin-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", + "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", + "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", "cpu": [ "arm64" ], @@ -1465,9 +1774,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", + "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", "cpu": [ "x64" ], @@ -1481,9 +1790,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", + "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", "cpu": [ "arm" ], @@ -1500,9 +1809,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", + "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", "cpu": [ "arm64" ], @@ -1519,9 +1828,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", + "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", "cpu": [ "ppc64" ], @@ -1538,9 +1847,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", + "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", "cpu": [ "riscv64" ], @@ -1557,9 +1866,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", + "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", "cpu": [ "s390x" ], @@ -1576,9 +1885,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz", + "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==", "cpu": [ "x64" ], @@ -1595,9 +1904,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz", + "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==", "cpu": [ "arm64" ], @@ -1614,9 +1923,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz", + "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==", "cpu": [ "x64" ], @@ -1633,9 +1942,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz", + "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==", "cpu": [ "arm" ], @@ -1648,19 +1957,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" + "@img/sharp-libvips-linux-arm": "1.3.2" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz", + "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==", "cpu": [ "arm64" ], @@ -1673,19 +1982,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" + "@img/sharp-libvips-linux-arm64": "1.3.2" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz", + "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==", "cpu": [ "ppc64" ], @@ -1698,19 +2007,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" + "@img/sharp-libvips-linux-ppc64": "1.3.2" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz", + "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==", "cpu": [ "riscv64" ], @@ -1723,19 +2032,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" + "@img/sharp-libvips-linux-riscv64": "1.3.2" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz", + "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==", "cpu": [ "s390x" ], @@ -1748,19 +2057,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" + "@img/sharp-libvips-linux-s390x": "1.3.2" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz", + "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==", "cpu": [ "x64" ], @@ -1773,19 +2082,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" + "@img/sharp-libvips-linux-x64": "1.3.2" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz", + "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==", "cpu": [ "arm64" ], @@ -1798,19 +2107,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz", + "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==", "cpu": [ "x64" ], @@ -1823,38 +2132,54 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + "@img/sharp-libvips-linuxmusl-x64": "1.3.2" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", + "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz", + "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==", "cpu": [ "wasm32" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.7.0" + "@img/sharp-wasm32": "0.35.3" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz", + "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==", "cpu": [ "arm64" ], @@ -1864,16 +2189,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz", + "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==", "cpu": [ "ia32" ], @@ -1883,16 +2208,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "^20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz", + "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==", "cpu": [ "x64" ], @@ -1902,7 +2227,7 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" @@ -1952,18 +2277,39 @@ } }, "node_modules/@mermaid-js/parser": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.1.1.tgz", - "integrity": "sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.2.0.tgz", + "integrity": "sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==", + "license": "MIT", + "dependencies": { + "@chevrotain/types": "~11.1.2" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz", + "integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==", "license": "MIT", + "optional": true, "dependencies": { - "@chevrotain/types": "~11.1.1" + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "node_modules/@nodable/entities": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.1.tgz", - "integrity": "sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.0.0.tgz", + "integrity": "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==", "funding": [ { "type": "github", @@ -1978,6 +2324,15 @@ "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", "license": "MIT" }, + "node_modules/@oxc-project/types": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz", + "integrity": "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@pagefind/darwin-arm64": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/@pagefind/darwin-arm64/-/darwin-arm64-1.5.2.tgz", @@ -2127,64 +2482,26 @@ "node": ">=18" } }, - "node_modules/@rollup/pluginutils": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", - "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz", + "integrity": "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==", + "cpu": [ + "arm64" + ], "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rollup/pluginutils/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.61.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.0.tgz", - "integrity": "sha512-dnxczajOqt0gesZlN5pGQ1s1imQVrsmCw5G2Ci4oM+0WvNz3pyRnlWrT7McoZIb8VlFwCawdmbWRmxRn7HI+VQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.61.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.0.tgz", - "integrity": "sha512-Bp3JpGP00Vu3f238ivRrjf7z3xSzVPXqCmaJYA9t2c+c8vKYvOzmXF7LkkeUalTEGd6cZcSWe+PFIP3Vy48fRg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.61.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.0.tgz", - "integrity": "sha512-zaYIpr670mUmmZ1tVzUFplbQbG7h3Gugx3L5FoqhsC2m/YnLlR1a7zVLmXNPy+iY1tFPEbNG+HHBXZGyId0G5w==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==", "cpu": [ "arm64" ], @@ -2192,12 +2509,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.61.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.0.tgz", - "integrity": "sha512-+P49fvkv2dSoeevUW+lgZ/I2JHSsJCK1Lyjj7Cu6E4UHG4tS9XIefzIjo5qhgELjAclnen1rLzK2PMKJdo+Dyg==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.4.tgz", + "integrity": "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==", "cpu": [ "x64" ], @@ -2205,25 +2525,15 @@ "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.61.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.0.tgz", - "integrity": "sha512-l3FAAOyKJXH2ea6KNFN+MMgC/rnE94YGLXs2ehYqDcCoHt1DpvgWX75BhUJxN38XojP7Ul+4H8PRn7EdyqSDrw==", - "cpu": [ - "arm64" ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.61.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.0.tgz", - "integrity": "sha512-VokPN3TSctKj65cyCNPaUh4vMFA8awxOot/0sp+4J7ZlNRKQEhXhawqPwajoi8H5ZFt61i0ugZJuTKXBjGJ17Q==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.4.tgz", + "integrity": "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==", "cpu": [ "x64" ], @@ -2231,79 +2541,34 @@ "optional": true, "os": [ "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.61.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.0.tgz", - "integrity": "sha512-DxH0P3wxm+Yzs/p3zrk9dw1rURu8p0Nv5+MRK/L7OtnLNg5rLZraSBFZ8iUXOd9f2BlhJyEpIZUH/emjq4UJ4g==", - "cpu": [ - "arm" - ], - "libc": [ - "glibc" ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.61.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.0.tgz", - "integrity": "sha512-T6ZvMNe84kAz6TBWHC7hGAoEtzP1LWYw/AqayGWEF6uISt3Abk/st06LqRD9THd7Xz3NxzurUpzAuEAUbZf+nw==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.4.tgz", + "integrity": "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==", "cpu": [ "arm" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.61.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.0.tgz", - "integrity": "sha512-q/4hzvQkDs8b4jIBab1pnLiiM0ayTZsN2amBFPDzuyZxjEd4wDwx0UJFYM3cOZzSf5Kw8fnWSprJzIBMkcR44Q==", - "cpu": [ - "arm64" ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.61.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.0.tgz", - "integrity": "sha512-vvYWX3akdEAY6km+9wAqFDnk6pQsbJKVnj7xawcvs/+fdlYBGp+U+Qq/lLfpIxYIZvZLHMAKD9HLdacSx/r3dw==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.4.tgz", + "integrity": "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==", "cpu": [ "arm64" ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.61.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.0.tgz", - "integrity": "sha512-DePa5cqOxDP/Zp0VOXpeWaGew5iIv5DXp9NYbzkX5PFQyWVX9184WCTh3hvr/7lhXo8ZVlbFLkz8+o/q1dU6gA==", - "cpu": [ - "loong64" - ], "libc": [ "glibc" ], @@ -2311,14 +2576,17 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.61.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.0.tgz", - "integrity": "sha512-LV8aWMB8UChglMCEzs7RkN0GsH29RJaLLqwm9fCIjlqwxQTiWAqNcc7wjBkH31hV0PU/yVxGYvrYsgfea2qw6g==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.4.tgz", + "integrity": "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==", "cpu": [ - "loong64" + "arm64" ], "libc": [ "musl" @@ -2327,47 +2595,18 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.61.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.0.tgz", - "integrity": "sha512-QoNSnwQtaeNu5grdBbsL0tt1uyl5EnS8DA8Mr3nluMXbhdQNyhN+G4tBax7VCdxLKj8YJ0/4OO9Ho84jMnJtKA==", - "cpu": [ - "ppc64" ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.61.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.0.tgz", - "integrity": "sha512-/zZp5MKapIIApE8trN8qLGNSiRN9TUoaUZ1cmVu4XnVdd5LQLOXTtyi+vtfUbNnT3iyjzpPqYeKXmvJ+gJGYWw==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.4.tgz", + "integrity": "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==", "cpu": [ "ppc64" ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.61.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.0.tgz", - "integrity": "sha512-RbrzcD3aJ1k3UbtMRRBNwojdVVyXjuVAFTfn/xPa6EEl6GE9Sm/akPgFTb9aAC9pMKGJ6CtWxaGrqWcabH+ySg==", - "cpu": [ - "riscv64" - ], "libc": [ "glibc" ], @@ -2375,28 +2614,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.61.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.0.tgz", - "integrity": "sha512-ZF+onDsBso8PJf1XaG9lB+O9RnBpKGnY6OrzC4CSHrtC1jb6jWLTKK4bRqdoCXHd22gyr2hiYmEAm8Wns/BOCw==", - "cpu": [ - "riscv64" ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.61.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.0.tgz", - "integrity": "sha512-Atk0aSIk5Zx2Wuh9dgRQgLP0Koc8hOeYpbWryMXyk8G8/HmPkwPPkMqIIDhrXHHYqfUzSJA/I7IWSBv8xSmRBA==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.4.tgz", + "integrity": "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==", "cpu": [ "s390x" ], @@ -2407,12 +2633,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.61.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.0.tgz", - "integrity": "sha512-0uMOcf3eZ5K+K4cYHkdxShFMPlPXCOdfDFEFn9dNYAEEd2cVvmOfH7zFgRVoDgmtQ1m9k5q7qfrHzyMAubKYUA==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.4.tgz", + "integrity": "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==", "cpu": [ "x64" ], @@ -2423,12 +2652,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.61.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.0.tgz", - "integrity": "sha512-mvFtE4A/t/7hRJ7X8Ozmu8FsIkAUat2nzl12pgU337BRmq87AQUJztwHz2Zv5/tjo9/C95E66CK03SI/ToEDJw==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.4.tgz", + "integrity": "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==", "cpu": [ "x64" ], @@ -2439,25 +2671,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.61.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.0.tgz", - "integrity": "sha512-z9b9+aTxvt8n2rNltMPvyaUfB8NJ+CVyOrGK/MdIKHx7B+lXmZpm/XbRsU7Rpf3fRqJ2uS6mBJiJveCtq8LHDg==", - "cpu": [ - "x64" ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.61.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.0.tgz", - "integrity": "sha512-jXaXFqKMehsOc+g8R6oo33RRC6w07G9jDBxAE5eAKX7mOcCbZloYIPNhfG9Wl+P9O9IWHFO4OJgPi1Ml2qkt7w==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.4.tgz", + "integrity": "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==", "cpu": [ "arm64" ], @@ -2465,12 +2687,15 @@ "optional": true, "os": [ "openharmony" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.61.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.0.tgz", - "integrity": "sha512-OXNWVFocS2IA4+QplhTZZ2a+8hPZR7T8KuozsNmJKK8y7cp83StHvGksfHzPG3wczWTczyWHVQuqeiTUbjiyBg==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.4.tgz", + "integrity": "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==", "cpu": [ "arm64" ], @@ -2478,25 +2703,15 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.61.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.0.tgz", - "integrity": "sha512-AlAbNtBO637LxSldqV43z0FfXoGfl2TW1DgAg/bs7aQswFbDewz2SJm3BUhiGfbOVtW571xbc9p+REdxhyN/Eg==", - "cpu": [ - "ia32" ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.61.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.0.tgz", - "integrity": "sha512-QRSrQXyJ1M4tjNXdR0/G/IgV6lzfQQJYBjlWIEYkY2Xs86DRl/iEpQ4blMDjJxSl7n19eDKKXMg0AmuBVYy8pQ==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.4.tgz", + "integrity": "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==", "cpu": [ "x64" ], @@ -2504,20 +2719,16 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.61.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.0.tgz", - "integrity": "sha512-tkuFxhvKO/HlGd0VsINF6vHSYH8AF8W0TcNxKDK6JZmrehngFj78pToc8iemtnvwilDjs2G/qSzYFhe9U8q+fw==", - "cpu": [ - "x64" ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" }, "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", @@ -2527,15 +2738,15 @@ "license": "MIT" }, "node_modules/@shikijs/core": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.1.0.tgz", - "integrity": "sha512-jLJtSJeuFffqX6/inRE1zqU5aFv2hrszvYgq3OjbAgFRZiWv7abKMDdQzYxuSDfmUPQozZvI/kuy6VMTvnvqTQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.4.3.tgz", + "integrity": "sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==", "license": "MIT", "dependencies": { - "@shikijs/primitive": "4.1.0", - "@shikijs/types": "4.1.0", + "@shikijs/primitive": "4.4.3", + "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4", + "@types/hast": "^3.0.5", "hast-util-to-html": "^9.0.5" }, "engines": { @@ -2543,12 +2754,12 @@ } }, "node_modules/@shikijs/engine-javascript": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.1.0.tgz", - "integrity": "sha512-YquhawCUgaBfhsS72e2Y/dI59gCBNPHu3fEO/tvLaXrTssxZrY5ddjtNLTwndrMgPo8b3IscE+xoICDzpTmlFQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.4.3.tgz", + "integrity": "sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.1.0", + "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" }, @@ -2557,12 +2768,12 @@ } }, "node_modules/@shikijs/engine-oniguruma": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.1.0.tgz", - "integrity": "sha512-axLpjVs45YBvvINa+dJF+NPW+KtFkNXsFr4SDw2BMj9GdeMnGxVB9PQb2xXlJYovslt/nz6giedAyOANkfc7hg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.4.3.tgz", + "integrity": "sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.1.0", + "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2" }, "engines": { @@ -2570,51 +2781,51 @@ } }, "node_modules/@shikijs/langs": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.1.0.tgz", - "integrity": "sha512-nwOMruEkbgdZfQ/b8CgpNBVOpvG1k0N5tbmgiFeqsan401+x3ILqlzZJowSla4Agmq4hG2Uf2wh5jLTEhR8VSg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.4.3.tgz", + "integrity": "sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.1.0" + "@shikijs/types": "4.4.3" }, "engines": { "node": ">=20" } }, "node_modules/@shikijs/primitive": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.1.0.tgz", - "integrity": "sha512-zx2/2Uwj2q9X3KSyYREEhXO23xBw5WUhP4orK2lE4r+t9JGITmEe0JH+wPmJhqHpOT2bRRs6lAL945+LDvOAGw==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.4.3.tgz", + "integrity": "sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.1.0", + "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" + "@types/hast": "^3.0.5" }, "engines": { "node": ">=20" } }, "node_modules/@shikijs/themes": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.1.0.tgz", - "integrity": "sha512-emCcTnUM7yO2wltYbaxm+yLvcCI4+h8XBKc4KmJ7EZUXoSGjcCHifkI//R4OFit9ewpg7H2/9tjOuXrT2v/Knw==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.4.3.tgz", + "integrity": "sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.1.0" + "@shikijs/types": "4.4.3" }, "engines": { "node": ">=20" } }, "node_modules/@shikijs/types": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.1.0.tgz", - "integrity": "sha512-3EQWX54fMpniOrDblzAhiwiJwpiTMW6+B9DWyUd9ska483tbayFYuw47UxwuPknI31bKnySfVQ/QW+jFL4rFdA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.4.3.tgz", + "integrity": "sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g==", "license": "MIT", "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" + "@types/hast": "^3.0.5" }, "engines": { "node": ">=20" @@ -2627,29 +2838,29 @@ "license": "MIT" }, "node_modules/@simple-libs/child-process-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-1.0.2.tgz", - "integrity": "sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-2.0.0.tgz", + "integrity": "sha512-dvNoRKLijXnD0XoJAz94pbNuB5GQgDr55UhpSPhffDkTT0Cmcqh9jSCOtwfT2d4H6MI9E7c4SgtMuJXZ6F3c6A==", "dev": true, "license": "MIT", "dependencies": { - "@simple-libs/stream-utils": "^1.2.0" + "@simple-libs/stream-utils": "^2.0.0" }, "engines": { - "node": ">=18" + "node": ">=22" }, "funding": { "url": "https://ko-fi.com/dangreen" } }, "node_modules/@simple-libs/stream-utils": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz", - "integrity": "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-2.0.0.tgz", + "integrity": "sha512-fCTuZK4QBa+39Oz9l4OGfJfz+GpwCp3AqO7Zch3to99xHPgstVsRFpeQ8LNd2o1Gv8raL2mCFwiaHh7bFSp5DQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=22" }, "funding": { "url": "https://ko-fi.com/dangreen" @@ -2668,6 +2879,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/braces": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/braces/-/braces-3.0.5.tgz", @@ -2814,9 +3035,9 @@ "license": "MIT" }, "node_modules/@types/d3-geo": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", - "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==", "license": "MIT", "dependencies": { "@types/geojson": "*" @@ -2856,9 +3077,9 @@ "license": "MIT" }, "node_modules/@types/d3-random": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", - "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.4.tgz", + "integrity": "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==", "license": "MIT" }, "node_modules/@types/d3-scale": { @@ -2959,9 +3180,9 @@ "license": "MIT" }, "node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", "license": "MIT", "dependencies": { "@types/unist": "*" @@ -2990,9 +3211,9 @@ } }, "node_modules/@types/mdx": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", - "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.14.tgz", + "integrity": "sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==", "license": "MIT" }, "node_modules/@types/micromatch": { @@ -3021,12 +3242,12 @@ } }, "node_modules/@types/node": { - "version": "25.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", - "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", "license": "MIT", "dependencies": { - "undici-types": ">=7.24.0 <7.24.7" + "undici-types": "~8.3.0" } }, "node_modules/@types/picomatch": { @@ -3059,9 +3280,9 @@ "license": "MIT" }, "node_modules/@ungap/structured-clone": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", - "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", "license": "ISC" }, "node_modules/@upsetjs/venn.js": { @@ -3180,9 +3401,9 @@ "license": "MIT" }, "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -3232,6 +3453,28 @@ } } }, + "node_modules/ajv-i18n": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ajv-i18n/-/ajv-i18n-4.2.0.tgz", + "integrity": "sha512-v/ei2UkCEeuKNXh8RToiFsUclmU+G57LO1Oo22OagNMENIw+Yb8eMwvHu7Vn9fmkjJyv6XclhJ8TbuigSglPkg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.0.0-beta.0" + } + }, + "node_modules/am-i-vibing": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/am-i-vibing/-/am-i-vibing-0.4.0.tgz", + "integrity": "sha512-MxT4XZL7pzLHpuvhDKdMaQHMGGkJDLluKBLsbstn+8wv9sWcFT6h+0ve9qkml95amVTZtZV83gQe2hY+ojgHLg==", + "license": "MIT", + "dependencies": { + "process-ancestry": "^0.1.0" + }, + "bin": { + "am-i-vibing": "dist/cli.mjs" + } + }, "node_modules/ansi-escapes": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", @@ -3249,9 +3492,9 @@ } }, "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", "dev": true, "license": "MIT", "engines": { @@ -3262,13 +3505,16 @@ } }, "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=12" + "node": ">=8" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" @@ -3299,6 +3545,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/anynum": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", + "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -3311,6 +3569,19 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, + "node_modules/argue-cli": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/argue-cli/-/argue-cli-3.1.0.tgz", + "integrity": "sha512-DhBpBfXL4SS2uC0N922MMajKR3CdrTG0u2or1PNYgXMsrSzViJrbtvT0nCLlLGUI0plam/ZZCs7aAauHtW9thw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://ko-fi.com/dangreen" + } + }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -3320,13 +3591,6 @@ "node": ">= 0.4" } }, - "node_modules/array-ify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", - "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", - "dev": true, - "license": "MIT" - }, "node_modules/array-iterate": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/array-iterate/-/array-iterate-2.0.1.tgz", @@ -3347,49 +3611,48 @@ } }, "node_modules/astro": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/astro/-/astro-6.4.3.tgz", - "integrity": "sha512-heArIk8zLcxuoj1WgBH2zGdAD8zKSU1mEcBvS6hYMEHRPlbtvB+4Y8ri9Z27hzeryvGaFgrH32zjghEfV2y07g==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/astro/-/astro-7.2.1.tgz", + "integrity": "sha512-ynyMTiyF//GLcd8+gRNbvcKgS1ht+qTgp/fkkHIpGYNILLseQqQRuPZ9PqXYKfsact4J1emZaXRDpg2bl35Pag==", "license": "MIT", "dependencies": { - "@astrojs/compiler": "^4.0.0", - "@astrojs/internal-helpers": "0.10.0", - "@astrojs/markdown-remark": "7.2.0", - "@astrojs/telemetry": "3.3.2", + "@astrojs/compiler-rs": "^0.3.2", + "@astrojs/internal-helpers": "0.10.2", + "@astrojs/markdown-satteri": "0.3.5", + "@astrojs/telemetry": "3.3.3", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", "@oslojs/encoding": "^1.1.0", - "@rollup/pluginutils": "^5.3.0", + "am-i-vibing": "^0.4.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", - "cookie": "^1.1.1", + "cookie": "^2.0.1", "devalue": "^5.8.1", "diff": "^8.0.3", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", - "esbuild": "^0.27.3", + "esbuild": "^0.28.0", "flattie": "^1.1.1", "fontace": "~0.4.1", "get-tsconfig": "5.0.0-beta.4", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", - "js-yaml": "^4.1.1", + "js-yaml": "^4.3.0", "jsonc-parser": "^3.3.1", - "magic-string": "^0.30.21", + "magic-string": "^1.0.0", "magicast": "^0.5.2", "mrmime": "^2.0.1", - "neotraverse": "^0.6.18", + "neotraverse": "^1.0.1", "obug": "^2.1.1", "p-limit": "^7.3.0", "p-queue": "^9.1.0", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.4", - "rehype": "^13.0.2", "semver": "^7.7.4", "shiki": "^4.0.2", "smol-toml": "^1.6.0", @@ -3399,10 +3662,8 @@ "tinyglobby": "^0.2.15", "ultrahtml": "^1.6.0", "unifont": "~0.7.4", - "unist-util-visit": "^5.1.0", "unstorage": "^1.17.5", - "vfile": "^6.0.3", - "vite": "^7.3.2", + "vite": "^8.0.13", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", @@ -3421,25 +3682,34 @@ "url": "https://opencollective.com/astrodotbuild" }, "optionalDependencies": { - "sharp": "^0.34.0" + "sharp": "^0.34.0 || ^0.35.0" + }, + "peerDependencies": { + "@astrojs/markdown-remark": "7.2.2" + }, + "peerDependenciesMeta": { + "@astrojs/markdown-remark": { + "optional": true + } } }, "node_modules/astro-expressive-code": { - "version": "0.42.0", - "resolved": "https://registry.npmjs.org/astro-expressive-code/-/astro-expressive-code-0.42.0.tgz", - "integrity": "sha512-aiTePi2Cn0mJPYWZSzP1GcxCinX9mNtJyCCshVVPSg1yRwM7ADvFJOx0FnS440M9t65hp8JH//dc2qr22Bm4ag==", + "version": "0.44.1", + "resolved": "https://registry.npmjs.org/astro-expressive-code/-/astro-expressive-code-0.44.1.tgz", + "integrity": "sha512-DT1LnCqbHasBKlvzJ3m6LR4VI94wwx3W9EV/YbP1te4rqjOHsvsezHYuqb5MeLWLftXms/1FA9QBbwCo43DnJQ==", "license": "MIT", "dependencies": { - "rehype-expressive-code": "^0.42.0" + "rehype-expressive-code": "^0.44.1", + "url-extras": "^0.1.0" }, "peerDependencies": { - "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta" + "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta || ^7.0.0" } }, "node_modules/astro-mermaid": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/astro-mermaid/-/astro-mermaid-2.0.2.tgz", - "integrity": "sha512-ihx63qwZ0hlu9mDjs6auQEXyo13s9h5HFHFIHovjTJH6ot97u0VqyGk3P1kkjPYZeOMrZ1Y7QAOevUfLs9cDfA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/astro-mermaid/-/astro-mermaid-2.1.0.tgz", + "integrity": "sha512-fFRUN0BTZh+DZhDiLyblXoO26XqJ1Rr+qK3JGgSu7OBspKHDm59jkztg/aHsrdo1vO/tIq/+xhP/vgT8Mp92XA==", "license": "MIT", "dependencies": { "import-meta-resolve": "^4.2.0", @@ -3457,59 +3727,13 @@ } } }, - "node_modules/astro-remote": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/astro-remote/-/astro-remote-0.3.4.tgz", - "integrity": "sha512-jL5skNQLA0YBc1R3bVGXyHew3FqGqsT7AgLzWAVeTLzFkwVMUYvs4/lKJSmS7ygcF1GnHnoKG6++8GL9VtWwGQ==", + "node_modules/astro/node_modules/magic-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.1.1.tgz", + "integrity": "sha512-qFemKPzc3ttrYVaMmnSkGtGc5nE6Ncl4bj7c9IE6C9OUIRXjf6PzJ+UZ1xhVIjYc7dolHq3qKzpAJPZUbvNj+A==", "license": "MIT", "dependencies": { - "entities": "^4.5.0", - "marked": "^12.0.0", - "marked-footnote": "^1.2.2", - "marked-smartypants": "^1.1.6", - "ultrahtml": "^1.5.3" - }, - "engines": { - "node": ">=18.14.1" - } - }, - "node_modules/astro-remote/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/astro-remote/node_modules/marked": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz", - "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/astro/node_modules/@astrojs/compiler": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-4.0.0.tgz", - "integrity": "sha512-eouss7G8ygdZqHuke033VMcVw5HTZUu+PXd/h06DGDUg/jt5btPYPqh66ENWw/mU78rBrf/oeC4oqoBwMtDMNA==", - "license": "MIT" - }, - "node_modules/astro/node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/asynckit": { @@ -3539,9 +3763,9 @@ } }, "node_modules/bcp-47": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.0.tgz", - "integrity": "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.1.tgz", + "integrity": "sha512-KLw+H/gd2p4zly1X7Yh/qziuyae5/w/QFnvTng9eZL5fvszL7Whl3MBoWF8yxL7ksUjBfOD+OxkytiqbBpG+Fw==", "license": "MIT", "dependencies": { "is-alphabetical": "^2.0.0", @@ -3633,22 +3857,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chalk/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/character-entities": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", @@ -3720,141 +3928,37 @@ "node": ">=8" } }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", - "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^8.0.0", - "string-width": "^8.2.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, "license": "ISC", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", "engines": { - "node": ">=8" + "node": ">=20" } }, "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/clsx": { @@ -3937,67 +4041,56 @@ "node": ">= 18" } }, - "node_modules/compare-func": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", - "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-ify": "^1.0.0", - "dot-prop": "^5.1.0" - } - }, "node_modules/conventional-changelog-angular": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-8.3.1.tgz", - "integrity": "sha512-6gfI3otXK5Ph5DfCOI1dblr+kN3FAm5a97hYoQkqNZxOaYa5WKfXH+AnpsmS+iUH2mgVC2Cg2Qw9m5OKcmNrIg==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-9.3.0.tgz", + "integrity": "sha512-0MWQLVUT1oVCsUGs9aAWteBVxPlLwJTn5VbQH7B0B3fDizZgrJ9QGnKl/2mp1+5P7153GCBCjO/v1aKJ6eysCg==", "dev": true, "license": "ISC", "dependencies": { - "compare-func": "^2.0.0" + "@conventional-changelog/template": "^1.3.0" }, "engines": { - "node": ">=18" + "node": ">=22" } }, "node_modules/conventional-changelog-conventionalcommits": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-9.3.1.tgz", - "integrity": "sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-10.3.0.tgz", + "integrity": "sha512-qag0zFD867Qq1DK0jAWicyWlEMS1FFC/BLVLISaoeI7Y6Em6aWchk7BCkhOTsecRpMsV6qX41XmlNnghiiTmSw==", "dev": true, "license": "ISC", "dependencies": { - "compare-func": "^2.0.0" + "@conventional-changelog/template": "^1.3.0" }, "engines": { - "node": ">=18" + "node": ">=22" } }, "node_modules/conventional-commits-parser": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-6.4.0.tgz", - "integrity": "sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-7.1.2.tgz", + "integrity": "sha512-O+x4N2yH+ijvqWlIyTHsXTAP+algNWgGbjY2duCe8w2vUMvUB95cLRslCPfTMQyLAKlet3bhZTdu6ozn4M+QJQ==", "dev": true, "license": "MIT", "dependencies": { - "@simple-libs/stream-utils": "^1.2.0", - "meow": "^13.0.0" + "@simple-libs/stream-utils": "^2.0.0", + "argue-cli": "^3.1.0" }, "bin": { "conventional-commits-parser": "dist/cli/index.js" }, "engines": { - "node": ">=18" + "node": ">=22" } }, "node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-2.0.1.tgz", + "integrity": "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==", "license": "MIT", "engines": { - "node": ">=18" + "node": ">=22" }, "funding": { "type": "opencollective", @@ -4020,9 +4113,9 @@ } }, "node_modules/cosmiconfig": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", - "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", "dev": true, "license": "MIT", "dependencies": { @@ -4191,9 +4284,9 @@ "license": "CC0-1.0" }, "node_modules/cytoscape": { - "version": "3.34.0", - "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.0.tgz", - "integrity": "sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==", + "version": "3.34.1", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.1.tgz", + "integrity": "sha512-Lr0RvH9H75y9ar8h9Toy6u4lxRSCcxUq+hHcQ26sVWo6BnaQp1gwEZOYqwuYTZhyW7npyKnNLP8oJ2p1/3OZ7g==", "license": "MIT", "engines": { "node": ">=0.10" @@ -4775,9 +4868,9 @@ } }, "node_modules/devalue": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", - "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.0.tgz", + "integrity": "sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A==", "license": "MIT" }, "node_modules/devlop": { @@ -4869,9 +4962,9 @@ } }, "node_modules/dompurify": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.7.tgz", - "integrity": "sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA==", + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz", + "integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -4891,19 +4984,6 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/dot-prop": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-obj": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/dset": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", @@ -5025,9 +5105,9 @@ } }, "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", "license": "MIT" }, "node_modules/es-object-atoms": { @@ -5060,13 +5140,14 @@ } }, "node_modules/es-toolkit": { - "version": "1.47.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.47.0.tgz", - "integrity": "sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==", + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.50.0.tgz", + "integrity": "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==", "license": "MIT", "workspaces": [ "docs", - "benchmarks" + "benchmarks", + "tests/types" ] }, "node_modules/esast-util-from-estree": { @@ -5102,9 +5183,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -5114,32 +5195,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" } }, "node_modules/escalade": { @@ -5305,15 +5386,15 @@ } }, "node_modules/expressive-code": { - "version": "0.42.0", - "resolved": "https://registry.npmjs.org/expressive-code/-/expressive-code-0.42.0.tgz", - "integrity": "sha512-V5DtJLEKuj4wf9O6IRtPtRObkMVy2ggR+S0MdjrTw6m58krZnDioyhW1si3Y04c5YPeooP4nd85Yq9NwEVHS4g==", + "version": "0.44.1", + "resolved": "https://registry.npmjs.org/expressive-code/-/expressive-code-0.44.1.tgz", + "integrity": "sha512-GakidxhapWDzpKLqEaFQ8wGk6gAqEtPQibu8+yPBfnDLgev5Vdsh1pasTxnrXL/mzIknyqeTwhMHTghdaiUrTg==", "license": "MIT", "dependencies": { - "@expressive-code/core": "^0.42.0", - "@expressive-code/plugin-frames": "^0.42.0", - "@expressive-code/plugin-shiki": "^0.42.0", - "@expressive-code/plugin-text-markers": "^0.42.0" + "@expressive-code/core": "^0.44.1", + "@expressive-code/plugin-frames": "^0.44.1", + "@expressive-code/plugin-shiki": "^0.44.1", + "@expressive-code/plugin-text-markers": "^0.44.1" } }, "node_modules/extend": { @@ -5345,9 +5426,9 @@ } }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", "dev": true, "funding": [ { @@ -5371,9 +5452,9 @@ } }, "node_modules/fast-xml-builder": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", - "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.0.tgz", + "integrity": "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ==", "funding": [ { "type": "github", @@ -5382,14 +5463,14 @@ ], "license": "MIT", "dependencies": { - "path-expression-matcher": "^1.5.0", - "xml-naming": "^0.1.0" + "path-expression-matcher": "^1.6.2", + "xml-naming": "^0.3.0" } }, "node_modules/fast-xml-parser": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.8.0.tgz", - "integrity": "sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg==", + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.10.1.tgz", + "integrity": "sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw==", "funding": [ { "type": "github", @@ -5398,11 +5479,12 @@ ], "license": "MIT", "dependencies": { - "@nodable/entities": "^2.1.0", + "@nodable/entities": "^3.0.0", "fast-xml-builder": "^1.2.0", - "path-expression-matcher": "^1.5.0", - "strnum": "^2.3.0", - "xml-naming": "^0.1.0" + "is-unsafe": "^2.0.0", + "path-expression-matcher": "^1.6.2", + "strnum": "^2.4.1", + "xml-naming": "^0.3.0" }, "bin": { "fxparser": "src/cli/cli.js" @@ -5741,23 +5823,6 @@ "win32" ] }, - "node_modules/git-raw-commits": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-5.0.1.tgz", - "integrity": "sha512-Y+csSm2GD/PCSh6Isd/WiMjNAydu0VBiG9J7EdQsNA5P9uXvLayqjmTsNlK5Gs9IhblFZqOU0yid5Il5JPoLiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@conventional-changelog/git-client": "^2.6.0", - "meow": "^13.0.0" - }, - "bin": { - "git-raw-commits": "src/cli.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/github-slugger": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", @@ -6320,9 +6385,116 @@ "node": ">=20" } }, - "node_modules/human-signals": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "node_modules/httpsnippet/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/httpsnippet/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/httpsnippet/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/httpsnippet/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/httpsnippet/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/httpsnippet/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/httpsnippet/node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/httpsnippet/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", "dev": true, "license": "Apache-2.0", @@ -6347,9 +6519,9 @@ } }, "node_modules/i18next": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.0.tgz", - "integrity": "sha512-gHSgGpUXVmuqE2El1W61DmxeyeTlFfZgdJRWMo9jScAn5pu7TuTuiccb1zh3E2J9hEBVGJ23+96x0ieBhfuIHA==", + "version": "26.3.6", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.6.tgz", + "integrity": "sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA==", "funding": [ { "type": "individual", @@ -6366,7 +6538,7 @@ ], "license": "MIT", "peerDependencies": { - "typescript": "^5 || ^6" + "typescript": "^5 || ^6 || ^7" }, "peerDependenciesMeta": { "typescript": { @@ -6527,19 +6699,13 @@ } }, "node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/is-hexadecimal": { @@ -6552,39 +6718,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "license": "MIT", - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-inside-container/node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -6596,13 +6729,13 @@ } }, "node_modules/is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, "node_modules/is-plain-obj": { @@ -6653,20 +6786,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-wsl": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", - "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", - "license": "MIT", - "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "node_modules/is-unsafe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.0.tgz", + "integrity": "sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", @@ -6693,9 +6823,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "funding": [ { "type": "github", @@ -6809,134 +6939,304 @@ "node": ">=6" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lint-staged": { - "version": "17.0.7", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.0.7.tgz", - "integrity": "sha512-JrSobt+tW3rH8IOMi8tDZd3foorM5yPEkLD/V2NxobgHrFfHWGee4MOLVuZeScgxftEwbHrPHIFA/ZL+nUJeuA==", - "dev": true, - "license": "MIT", + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "license": "MPL-2.0", "dependencies": { - "listr2": "^10.2.1", - "picomatch": "^4.0.4", - "string-argv": "^0.3.2", - "tinyexec": "^1.2.4" - }, - "bin": { - "lint-staged": "bin/lint-staged.js" + "detect-libc": "^2.0.3" }, "engines": { - "node": ">=22.22.1" + "node": ">= 12.0.0" }, "funding": { - "url": "https://opencollective.com/lint-staged" + "type": "opencollective", + "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "yaml": "^2.9.0" - } - }, - "node_modules/listr2": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-10.2.1.tgz", - "integrity": "sha512-7I5knELsJKTUjXG+A6BkKAiGkW1i25fNa/xlUl9hFtk15WbE9jndA89xu5FzQKrY5llajE1hfZZFMILXkDHk/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^5.2.0", - "eventemitter3": "^5.0.4", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^10.0.0" - }, + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=22.13.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/lodash-es": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", - "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", - "license": "MIT" - }, - "node_modules/log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", - "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/log-update/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lint-staged": { + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.3.0.tgz", + "integrity": "sha512-woZS3vNe3UKqBaLPvbLOtKRY4tLANpWQhom12MGWqC8Mh1lCOO+WgSwmX2amjJAqTY9BkXYW87fCUH5H9Ph6xw==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" + "picomatch": "^4.0.5", + "string-argv": "^0.3.2", + "tinyexec": "^1.2.4" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" }, "engines": { - "node": ">=18" + "node": ">=22.22.1" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://opencollective.com/lint-staged" + }, + "optionalDependencies": { + "yaml": "^2.9.0" } }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -6948,9 +7248,9 @@ } }, "node_modules/lru-cache": { - "version": "11.5.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", - "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -6966,13 +7266,13 @@ } }, "node_modules/magicast": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", - "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.4.tgz", + "integrity": "sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.3", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "source-map-js": "^1.2.1" } }, @@ -7017,36 +7317,6 @@ "node": ">= 20" } }, - "node_modules/marked-footnote": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/marked-footnote/-/marked-footnote-1.4.0.tgz", - "integrity": "sha512-fZTxAhI1TcLEs5UOjCfYfTHpyKGaWQevbxaGTEA68B51l7i87SctPFtHETYqPkEN0ka5opvy4Dy1l/yXVC+hmg==", - "license": "MIT", - "peerDependencies": { - "marked": ">=7.0.0" - } - }, - "node_modules/marked-plaintify": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/marked-plaintify/-/marked-plaintify-1.1.1.tgz", - "integrity": "sha512-r3kMKArhfo2H3lD4ctFq/OJTzM0uNvXHh7FBTI1hMDpf4Ac1djjtq4g8NfTBWMxWLmaEz3KL1jCkLygik3gExA==", - "license": "MIT", - "peerDependencies": { - "marked": ">=13.0.0" - } - }, - "node_modules/marked-smartypants": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/marked-smartypants/-/marked-smartypants-1.1.12.tgz", - "integrity": "sha512-Z0QL2GpihbSeG5aaCrQxMEoqvngMftF/gq1SrdlCnbecUSrX3HYgPtCZzCW+OyNe2ideQqaFdxfGryqQX1MBDA==", - "license": "MIT", - "dependencies": { - "smartypants": "^0.2.2" - }, - "peerDependencies": { - "marked": ">=4 <19" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -7386,40 +7656,27 @@ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "license": "CC0-1.0" }, - "node_modules/meow": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", - "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/mermaid": { - "version": "11.15.0", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.15.0.tgz", - "integrity": "sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==", + "version": "11.16.1", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.16.1.tgz", + "integrity": "sha512-TQsq6u22fAn3rek5VOubrhKPo1g5hwC3FXUN9hiyupTckcYiGuuKGkNQrKYwGJkXUxZdojwRG46gsSCFZMDp4g==", "license": "MIT", "dependencies": { - "@braintree/sanitize-url": "^7.1.1", + "@braintree/sanitize-url": "^7.1.2", "@iconify/utils": "^3.0.2", - "@mermaid-js/parser": "^1.1.1", + "@mermaid-js/parser": "^1.2.0", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", - "cytoscape": "^3.33.1", + "cytoscape": "^3.33.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", - "dayjs": "^1.11.19", - "dompurify": "^3.3.1", + "dayjs": "^1.11.20", + "dompurify": "^3.3.3", "es-toolkit": "^1.45.1", - "katex": "^0.16.25", + "katex": "^0.16.45", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", @@ -8214,19 +8471,6 @@ "node": ">= 0.6" } }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -8250,9 +8494,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", @@ -8268,9 +8512,9 @@ } }, "node_modules/neotraverse": { - "version": "0.6.18", - "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz", - "integrity": "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-1.0.1.tgz", + "integrity": "sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w==", "license": "MIT", "engines": { "node": ">= 10" @@ -8296,9 +8540,9 @@ "license": "MIT" }, "node_modules/node-mock-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz", - "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.5.tgz", + "integrity": "sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw==", "license": "MIT" }, "node_modules/normalize-path": { @@ -8353,14 +8597,17 @@ } }, "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", "funding": [ "https://github.com/sponsors/sxzz", "https://opencollective.com/debug" ], - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } }, "node_modules/ofetch": { "version": "1.5.1", @@ -8379,22 +8626,6 @@ "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", "license": "MIT" }, - "node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/oniguruma-parser": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", @@ -8421,9 +8652,9 @@ "peer": true }, "node_modules/p-limit": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.0.tgz", - "integrity": "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.1.tgz", + "integrity": "sha512-0trZaiG7Y7kN/Egy9a8j47t9osC0Tch4PaIWd9yGF6bvmlk7muExRvGNYb8sXBwEKMoNKsbNN9P8EefuQekE4Q==", "license": "MIT", "dependencies": { "yocto-queue": "^1.2.1" @@ -8436,9 +8667,9 @@ } }, "node_modules/p-queue": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.0.tgz", - "integrity": "sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang==", + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.3.tgz", + "integrity": "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA==", "license": "MIT", "dependencies": { "eventemitter3": "^5.0.4", @@ -8464,9 +8695,9 @@ } }, "node_modules/package-manager-detector": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", - "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz", + "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==", "license": "MIT" }, "node_modules/pagefind": { @@ -8601,9 +8832,9 @@ "license": "MIT" }, "node_modules/path-expression-matcher": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", - "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", + "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", "funding": [ { "type": "github", @@ -8651,9 +8882,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "license": "MIT", "engines": { "node": ">=12" @@ -8679,9 +8910,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "funding": [ { "type": "opencollective", @@ -8698,7 +8929,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -8732,9 +8963,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -8745,9 +8976,9 @@ } }, "node_modules/prettier": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", - "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", "dev": true, "license": "MIT", "bin": { @@ -8800,6 +9031,15 @@ "node": ">=6" } }, + "node_modules/process-ancestry": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/process-ancestry/-/process-ancestry-0.1.0.tgz", + "integrity": "sha512-tGqJW/UnclpYASFcM6Xh8D8l/BMtaQ9+CSG0vlJSJTcdMM4lDRv4c6H0Pdcsfted+bVczdYSfk2fdukg2gQkZg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/property-information": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", @@ -8938,12 +9178,12 @@ } }, "node_modules/rehype-expressive-code": { - "version": "0.42.0", - "resolved": "https://registry.npmjs.org/rehype-expressive-code/-/rehype-expressive-code-0.42.0.tgz", - "integrity": "sha512-8rp/1YMEVVSYbtz+bFBx+uSx3vA4i4T8RwRm5Q/IWbucQnnQqQ0hDqtmKOr8tv+59Cik6cu5aH3WPo0I7csuTA==", + "version": "0.44.1", + "resolved": "https://registry.npmjs.org/rehype-expressive-code/-/rehype-expressive-code-0.44.1.tgz", + "integrity": "sha512-+VZgs7Evw4LXRN3owpoBNSTpYuW6GeOdjqcUT1TuY8o/4MGPtbd0EU7Bgrju7X8KrQ6SslOBAuGWJ5fV5TriJQ==", "license": "MIT", "dependencies": { - "expressive-code": "^0.42.0" + "expressive-code": "^0.44.1" } }, "node_modules/rehype-format": { @@ -9135,9 +9375,9 @@ } }, "node_modules/remark-smartypants": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-3.0.2.tgz", - "integrity": "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-3.0.3.tgz", + "integrity": "sha512-gCaK+ndZ0hYezlqFegHFCVh2CQemsi0Npdh1qVM9bxlUFknjkbP6VmojWhddOCrbK0PbbacmYLWfTULRiT1eWA==", "license": "MIT", "dependencies": { "retext": "^9.0.0", @@ -9210,23 +9450,6 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/retext": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/retext/-/retext-9.0.0.tgz", @@ -9288,61 +9511,42 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true, - "license": "MIT" - }, "node_modules/robust-predicates": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", "license": "Unlicense" }, - "node_modules/rollup": { - "version": "4.61.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.0.tgz", - "integrity": "sha512-T9mWdbWfQtp0B5lv/HX+wrhYsmXRlcWnXXmJbXqKJhlRaoS6KMhq0gpyzW4UJfclcxrEdLnTgjT2NjruLONu0g==", + "node_modules/rolldown": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.4.tgz", + "integrity": "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==", "license": "MIT", "dependencies": { - "@types/estree": "1.0.9" + "@oxc-project/types": "=0.144.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.61.0", - "@rollup/rollup-android-arm64": "4.61.0", - "@rollup/rollup-darwin-arm64": "4.61.0", - "@rollup/rollup-darwin-x64": "4.61.0", - "@rollup/rollup-freebsd-arm64": "4.61.0", - "@rollup/rollup-freebsd-x64": "4.61.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.61.0", - "@rollup/rollup-linux-arm-musleabihf": "4.61.0", - "@rollup/rollup-linux-arm64-gnu": "4.61.0", - "@rollup/rollup-linux-arm64-musl": "4.61.0", - "@rollup/rollup-linux-loong64-gnu": "4.61.0", - "@rollup/rollup-linux-loong64-musl": "4.61.0", - "@rollup/rollup-linux-ppc64-gnu": "4.61.0", - "@rollup/rollup-linux-ppc64-musl": "4.61.0", - "@rollup/rollup-linux-riscv64-gnu": "4.61.0", - "@rollup/rollup-linux-riscv64-musl": "4.61.0", - "@rollup/rollup-linux-s390x-gnu": "4.61.0", - "@rollup/rollup-linux-x64-gnu": "4.61.0", - "@rollup/rollup-linux-x64-musl": "4.61.0", - "@rollup/rollup-openbsd-x64": "4.61.0", - "@rollup/rollup-openharmony-arm64": "4.61.0", - "@rollup/rollup-win32-arm64-msvc": "4.61.0", - "@rollup/rollup-win32-ia32-msvc": "4.61.0", - "@rollup/rollup-win32-x64-gnu": "4.61.0", - "@rollup/rollup-win32-x64-msvc": "4.61.0", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm64": "1.2.4", + "@rolldown/binding-darwin-arm64": "1.2.4", + "@rolldown/binding-darwin-x64": "1.2.4", + "@rolldown/binding-freebsd-x64": "1.2.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.4", + "@rolldown/binding-linux-arm64-gnu": "1.2.4", + "@rolldown/binding-linux-arm64-musl": "1.2.4", + "@rolldown/binding-linux-ppc64-gnu": "1.2.4", + "@rolldown/binding-linux-s390x-gnu": "1.2.4", + "@rolldown/binding-linux-x64-gnu": "1.2.4", + "@rolldown/binding-linux-x64-musl": "1.2.4", + "@rolldown/binding-openharmony-arm64": "1.2.4", + "@rolldown/binding-win32-arm64-msvc": "1.2.4", + "@rolldown/binding-win32-x64-msvc": "1.2.4" } }, "node_modules/roughjs": { @@ -9386,19 +9590,63 @@ "suf-log": "^2.5.3" } }, + "node_modules/satteri": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/satteri/-/satteri-0.9.5.tgz", + "integrity": "sha512-ZuWVl+vnM64y+/TtX8Kosv2c00W+hLQiiwnEL6H0UKVVrxFqMw4D2CJHHQaouVd89OAhtBBfjWLqhKi3TVUV4w==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.5", + "@types/hast": "^3.0.4", + "@types/mdast": "^4.0.4", + "@types/unist": "^3.0.3" + }, + "optionalDependencies": { + "@bruits/satteri-darwin-arm64": "0.9.5", + "@bruits/satteri-darwin-x64": "0.9.5", + "@bruits/satteri-linux-arm64-gnu": "0.9.5", + "@bruits/satteri-linux-arm64-musl": "0.9.5", + "@bruits/satteri-linux-x64-gnu": "0.9.5", + "@bruits/satteri-linux-x64-musl": "0.9.5", + "@bruits/satteri-wasm32-wasi": "0.9.5", + "@bruits/satteri-win32-arm64-msvc": "0.9.5", + "@bruits/satteri-win32-x64-msvc": "0.9.5" + } + }, "node_modules/sax": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", - "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" } }, + "node_modules/schema-dts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/schema-dts/-/schema-dts-2.0.0.tgz", + "integrity": "sha512-t7NoCy3Rn5GHGx6p7s1qIYK/AeIb8ZxJNR9WUNFkwMv2CiiGZBmqqYWc2FlZVm5ZbiHMY4OvBWhj7QtyrFO2Jw==", + "license": "Apache-2.0", + "dependencies": { + "schema-dts-lib": "^1.0.0" + } + }, + "node_modules/schema-dts-lib": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-dts-lib/-/schema-dts-lib-1.0.0.tgz", + "integrity": "sha512-9MEO5vpQH9JdBioUupqluzxSYxPLjhmqRUudk15adUl/ypnRsM2/M1kN3AmVJQeG7nZqcL68H8JlGqQQT6vy9A==", + "license": "Apache-2.0", + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + } + }, "node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -9408,47 +9656,52 @@ } }, "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", + "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==", "license": "Apache-2.0", "dependencies": { - "@img/colour": "^1.0.0", + "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", - "semver": "^7.7.3" + "semver": "^7.8.5" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" + "@img/sharp-darwin-arm64": "0.35.3", + "@img/sharp-darwin-x64": "0.35.3", + "@img/sharp-freebsd-wasm32": "0.35.3", + "@img/sharp-libvips-darwin-arm64": "1.3.2", + "@img/sharp-libvips-darwin-x64": "1.3.2", + "@img/sharp-libvips-linux-arm": "1.3.2", + "@img/sharp-libvips-linux-arm64": "1.3.2", + "@img/sharp-libvips-linux-ppc64": "1.3.2", + "@img/sharp-libvips-linux-riscv64": "1.3.2", + "@img/sharp-libvips-linux-s390x": "1.3.2", + "@img/sharp-libvips-linux-x64": "1.3.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", + "@img/sharp-libvips-linuxmusl-x64": "1.3.2", + "@img/sharp-linux-arm": "0.35.3", + "@img/sharp-linux-arm64": "0.35.3", + "@img/sharp-linux-ppc64": "0.35.3", + "@img/sharp-linux-riscv64": "0.35.3", + "@img/sharp-linux-s390x": "0.35.3", + "@img/sharp-linux-x64": "0.35.3", + "@img/sharp-linuxmusl-arm64": "0.35.3", + "@img/sharp-linuxmusl-x64": "0.35.3", + "@img/sharp-webcontainers-wasm32": "0.35.3", + "@img/sharp-win32-arm64": "0.35.3", + "@img/sharp-win32-ia32": "0.35.3", + "@img/sharp-win32-x64": "0.35.3" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/shebang-command": { @@ -9475,19 +9728,19 @@ } }, "node_modules/shiki": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.1.0.tgz", - "integrity": "sha512-l/ABZPUR5v70jI10EzqfMS/I96vjSGv2y0ihUV+WYFzv0EfvW4s54m0Lg8wCrrL+2IkwBzFTuxkZjPf8b2NX9Q==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.4.3.tgz", + "integrity": "sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g==", "license": "MIT", "dependencies": { - "@shikijs/core": "4.1.0", - "@shikijs/engine-javascript": "4.1.0", - "@shikijs/engine-oniguruma": "4.1.0", - "@shikijs/langs": "4.1.0", - "@shikijs/themes": "4.1.0", - "@shikijs/types": "4.1.0", + "@shikijs/core": "4.4.3", + "@shikijs/engine-javascript": "4.4.3", + "@shikijs/engine-oniguruma": "4.4.3", + "@shikijs/langs": "4.4.3", + "@shikijs/themes": "4.4.3", + "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" + "@types/hast": "^3.0.5" }, "engines": { "node": ">=20" @@ -9532,52 +9785,25 @@ } }, "node_modules/sitemap/node_modules/@types/node": { - "version": "24.12.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", - "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~7.18.0" } }, "node_modules/sitemap/node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "license": "MIT" }, - "node_modules/slice-ansi": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", - "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.3", - "is-fullwidth-code-point": "^5.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/smartypants": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/smartypants/-/smartypants-0.2.2.tgz", - "integrity": "sha512-TzobUYoEft/xBtb2voRPryAUIvYguG0V7Tt3de79I1WfXgCwelqVsGuZSnu3GFGRZhXR90AeEYIM+icuB/S06Q==", - "license": "BSD-3-Clause", - "bin": { - "smartypants": "bin/smartypants.js", - "smartypantsu": "bin/smartypantsu.js" - } - }, - "node_modules/smol-toml": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", - "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", - "license": "BSD-3-Clause", + "node_modules/smol-toml": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.8.0.tgz", + "integrity": "sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ==", + "license": "BSD-3-Clause", "engines": { "node": ">= 18" }, @@ -9613,6 +9839,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "license": "CC0-1.0" + }, "node_modules/split": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", @@ -9627,22 +9859,21 @@ } }, "node_modules/starlight-blog": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/starlight-blog/-/starlight-blog-0.26.1.tgz", - "integrity": "sha512-c2aLtkVTNFKHDJrh1DwlxBj+VyOrLqJeHmGkKKHwL6oowUoaqUiC/vyOZLXNmZL1kcYI0hgHDw1t4G/lkg8mZg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/starlight-blog/-/starlight-blog-0.28.0.tgz", + "integrity": "sha512-tKddmNbbUd20lV/m4Giih2RtDEegpnzPbrNdiwgI29XOCzVKXjtt9T9ckxTJU+cxwG1f0ii++J8duN9iBzw7Iw==", "license": "MIT", "dependencies": { - "@astrojs/markdown-remark": "^7.0.0", - "@astrojs/mdx": "^5.0.0", - "@astrojs/rss": "^4.0.17", - "astro-remote": "^0.3.4", + "@astrojs/markdown-remark": "^7.2.0", + "@astrojs/mdx": "^7.0.0", + "@astrojs/rss": "^4.0.18", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-html": "^9.0.5", "hast-util-to-string": "^3.0.1", - "marked": "^17.0.4", - "marked-plaintify": "^1.1.1", "mdast-util-mdx-expression": "^2.0.1", + "satteri": "^0.9.3", + "schema-dts": "^2.0.0", "unist-util-is": "^6.0.1", "unist-util-remove": "^4.0.0", "unist-util-visit": "^5.1.0" @@ -9651,28 +9882,17 @@ "node": ">=22.12.0" }, "peerDependencies": { - "@astrojs/starlight": ">=0.38.0" - } - }, - "node_modules/starlight-blog/node_modules/marked": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.6.tgz", - "integrity": "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 20" + "@astrojs/starlight": ">=0.41.0" } }, "node_modules/starlight-links-validator": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/starlight-links-validator/-/starlight-links-validator-0.24.0.tgz", - "integrity": "sha512-bsZf77oRJmY92KWOcu3vYK8Y12KJNvO3jQca1BgOBs+XskNfjPXrkgVtT7ls/FnLoomfsIV0wLdJfJs7kzGojA==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/starlight-links-validator/-/starlight-links-validator-0.25.3.tgz", + "integrity": "sha512-kNE2F8fwdq7r8l6Zx2nH3SDnLlVu2VRmbmyV/nSpbtWzf9+S81H2VBZ6GRevIKbm8qOoYSZvXzNATT8ZXw9bmw==", "dev": true, "license": "MIT", "dependencies": { + "@astrojs/markdown-satteri": "^0.3.2", "@types/picomatch": "^4.0.2", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", @@ -9680,6 +9900,7 @@ "mdast-util-mdx-jsx": "^3.2.0", "mdast-util-to-hast": "^13.2.1", "picomatch": "^4.0.3", + "satteri": "^0.9.3", "terminal-link": "^5.0.0", "unist-util-visit": "^5.1.0", "yaml": "^2.8.3" @@ -9688,18 +9909,18 @@ "node": ">=22.12.0" }, "peerDependencies": { - "@astrojs/starlight": ">=0.38.0", - "astro": ">=6.0.0" + "@astrojs/starlight": ">=0.41.0", + "astro": ">=7.0.2" } }, "node_modules/starlight-llms-txt": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/starlight-llms-txt/-/starlight-llms-txt-0.10.0.tgz", - "integrity": "sha512-LgkSjkvdACsGHkFq1ES00F0BU4lRepjJoaUmOgxBxNWx4txwpySVPtntKdAvDvlhinyN0ZBRpnAsN/sVQ1UEfA==", + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/starlight-llms-txt/-/starlight-llms-txt-0.11.0.tgz", + "integrity": "sha512-83TU5zPgJhL9fXIWAOWjQjyQ6EKocshEjJyA4zOJjOqu/oxQsrTpYGYxjHLyfZe4LRD52N9eCuykSkaIYwUyDw==", "dev": true, "license": "MIT", "dependencies": { - "@astrojs/mdx": "^5.0.4", + "@astrojs/mdx": "^7.0.0", "@types/hast": "^3.0.4", "@types/micromatch": "^4.0.10", "github-slugger": "^2.0.0", @@ -9713,14 +9934,14 @@ "unist-util-remove": "^4.0.0" }, "peerDependencies": { - "@astrojs/starlight": ">=0.38.0", - "astro": "^6.0.0" + "@astrojs/starlight": ">=0.41.0", + "astro": "^7.0.0" } }, "node_modules/starlight-openapi": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/starlight-openapi/-/starlight-openapi-0.25.3.tgz", - "integrity": "sha512-erFtGyU1NI1mHmlRrVo/SWLdVvjFr1Qsxwz1bnOss/rOSBH3Tgq39rSlkXr9HWbSTnbjIpZkitgYw+s73zk5pA==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/starlight-openapi/-/starlight-openapi-0.26.0.tgz", + "integrity": "sha512-+WJnS7nIW42KfgmtBvTdj0Sx0LR+3kYPCdXMoiiMxKWDTF9TBd8KhTRDqNiPPrksej+gItZV/ROKwsXoF2/qZQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9733,9 +9954,9 @@ "node": ">=22.12.0" }, "peerDependencies": { - "@astrojs/markdown-remark": ">=7.0.0", - "@astrojs/starlight": ">=0.38.0", - "astro": ">=6.0.0" + "@astrojs/markdown-satteri": ">=0.3.2", + "@astrojs/starlight": ">=0.41.0", + "astro": ">=7.0.2" } }, "node_modules/stream-combiner": { @@ -9766,9 +9987,9 @@ } }, "node_modules/string-width": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", - "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", + "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", "dev": true, "license": "MIT", "dependencies": { @@ -9811,16 +10032,6 @@ "node": ">=4" } }, - "node_modules/stringify-object/node_modules/is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/strip-ansi": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", @@ -9851,16 +10062,19 @@ } }, "node_modules/strnum": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", - "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.1.tgz", + "integrity": "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" } ], - "license": "MIT" + "license": "MIT", + "dependencies": { + "anynum": "^1.0.1" + } }, "node_modules/style-to-js": { "version": "1.1.21", @@ -9910,9 +10124,9 @@ } }, "node_modules/supports-hyperlinks": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-4.4.0.tgz", - "integrity": "sha512-UKbpT93hN5Nr9go5UY7bopIB9YQlMz9nm/ct4IXt/irb5YRkn9WaqrOBJGZ5Pwvsd5FQzSVeYlGdXoCAPQZrPg==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-4.5.0.tgz", + "integrity": "sha512-ZW2OvfeCXrNTbLakPUzjQG922EeGCOteFSVoek5DKStTh898wf7zgtuFlzQN8HfZCxC3Eh02yJVrRW51hADf+w==", "dev": true, "license": "MIT", "dependencies": { @@ -9953,9 +10167,9 @@ } }, "node_modules/svgo": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.1.tgz", - "integrity": "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.2.tgz", + "integrity": "sha512-ekx94z1rRc5LDi6oSUaeRnYhd0UOJxdtQCL2rF8xpWxD3TPAsISWOrxezqGovqS38GRZOdpDfvQe3ts6F7nsng==", "license": "MIT", "dependencies": { "commander": "^11.1.0", @@ -10017,18 +10231,18 @@ "license": "MIT" }, "node_modules/tinyclip": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-0.1.13.tgz", - "integrity": "sha512-8OqlXQ35euK9+e7L68u8UwcODxkHoIkjbGsgXuARKNyQ5G6xt8nw1YPeMbxMLgCPFkToU+UEK5j05t2t8edKpQ==", + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-0.1.15.tgz", + "integrity": "sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A==", "license": "MIT", "engines": { "node": "^16.14.0 || >= 17.3.0" } }, "node_modules/tinyexec": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", - "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", "license": "MIT", "engines": { "node": ">=18" @@ -10068,523 +10282,65 @@ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/trim-trailing-lines": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-2.1.0.tgz", - "integrity": "sha512-5UR5Biq4VlVOtzqkm2AZlgvSlDJtME46uV0br0gENbwN4l5+mMKT4b9gJKqWtuL2zAIqajGJGuvbCbcAJUZqBg==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/trough": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", - "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/ts-dedent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", - "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", - "license": "MIT", - "engines": { - "node": ">=6.10" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true - }, - "node_modules/tsx": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", - "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.28.0" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", - "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/android-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", - "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/android-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", - "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/android-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", - "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", - "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/darwin-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", - "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", - "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", - "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", - "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", - "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", - "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-loong64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", - "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", - "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", - "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", - "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-s390x": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", - "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", - "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", - "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", - "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", - "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", - "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/sunos-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", - "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/tsx/node_modules/@esbuild/win32-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", - "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", - "cpu": [ - "arm64" - ], + "node_modules/trim-trailing-lines": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-2.1.0.tgz", + "integrity": "sha512-5UR5Biq4VlVOtzqkm2AZlgvSlDJtME46uV0br0gENbwN4l5+mMKT4b9gJKqWtuL2zAIqajGJGuvbCbcAJUZqBg==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/tsx/node_modules/@esbuild/win32-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", - "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", - "cpu": [ - "ia32" - ], + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/tsx/node_modules/@esbuild/win32-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", - "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", - "cpu": [ - "x64" - ], + "node_modules/ts-dedent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", + "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">=18" + "node": ">=6.10" } }, - "node_modules/tsx/node_modules/esbuild": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", - "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/tsx": { + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", "devOptional": true, - "hasInstallScript": true, "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, "bin": { - "esbuild": "bin/esbuild" + "tsx": "dist/cli.mjs" }, "engines": { - "node": ">=18" + "node": ">=18.0.0" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.0", - "@esbuild/android-arm": "0.28.0", - "@esbuild/android-arm64": "0.28.0", - "@esbuild/android-x64": "0.28.0", - "@esbuild/darwin-arm64": "0.28.0", - "@esbuild/darwin-x64": "0.28.0", - "@esbuild/freebsd-arm64": "0.28.0", - "@esbuild/freebsd-x64": "0.28.0", - "@esbuild/linux-arm": "0.28.0", - "@esbuild/linux-arm64": "0.28.0", - "@esbuild/linux-ia32": "0.28.0", - "@esbuild/linux-loong64": "0.28.0", - "@esbuild/linux-mips64el": "0.28.0", - "@esbuild/linux-ppc64": "0.28.0", - "@esbuild/linux-riscv64": "0.28.0", - "@esbuild/linux-s390x": "0.28.0", - "@esbuild/linux-x64": "0.28.0", - "@esbuild/netbsd-arm64": "0.28.0", - "@esbuild/netbsd-x64": "0.28.0", - "@esbuild/openbsd-arm64": "0.28.0", - "@esbuild/openbsd-x64": "0.28.0", - "@esbuild/openharmony-arm64": "0.28.0", - "@esbuild/sunos-x64": "0.28.0", - "@esbuild/win32-arm64": "0.28.0", - "@esbuild/win32-ia32": "0.28.0", - "@esbuild/win32-x64": "0.28.0" + "fsevents": "~2.3.3" } }, "node_modules/typesafe-path": { @@ -10598,7 +10354,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -10625,9 +10380,9 @@ "license": "MIT" }, "node_modules/ultrahtml": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.6.0.tgz", - "integrity": "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.7.0.tgz", + "integrity": "sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g==", "license": "MIT" }, "node_modules/uncrypto": { @@ -10636,10 +10391,19 @@ "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", "license": "MIT" }, + "node_modules/undici": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz", + "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==", + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, "node_modules/undici-types": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", - "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "license": "MIT" }, "node_modules/unicorn-magic": { @@ -10675,14 +10439,14 @@ } }, "node_modules/unifont": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/unifont/-/unifont-0.7.4.tgz", - "integrity": "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg==", + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/unifont/-/unifont-0.7.5.tgz", + "integrity": "sha512-ULe/Cs+ZIsq+dcFofNkhqielCrUJnb5mr+Yc4EBM2VlL+6OZR6+cjtI2mT1bJvRBrVncqHAbLURxmPLcCXzWMg==", "license": "MIT", "dependencies": { "css-tree": "^3.1.0", - "ofetch": "^1.5.1", - "ohash": "^2.0.11" + "ohash": "^2.0.11", + "undici": "^8.0.0" } }, "node_modules/unist-util-find-after": { @@ -10948,9 +10712,9 @@ } }, "node_modules/unstorage/node_modules/readdirp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", - "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", "license": "MIT", "engines": { "node": ">= 20.19.0" @@ -10960,6 +10724,18 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/url-extras": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/url-extras/-/url-extras-0.1.0.tgz", + "integrity": "sha512-8tzwTeXFPuX/5PHuCDQE5Dd9Ts4rwoq2t9aIT+HS4iAVpmj5l4Ao7Q+BuuFjvWRqrLswBhQDk8O96ZicgCqQqw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/url-template": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/url-template/-/url-template-3.1.1.tgz", @@ -10977,9 +10753,9 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", - "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -11032,17 +10808,16 @@ } }, "node_modules/vite": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", - "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -11058,9 +10833,10 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", - "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -11073,13 +10849,16 @@ "@types/node": { "optional": true }, - "jiti": { + "@vitejs/devtools": { "optional": true }, - "less": { + "esbuild": { + "optional": true + }, + "jiti": { "optional": true }, - "lightningcss": { + "less": { "optional": true }, "sass": { @@ -11125,9 +10904,9 @@ } }, "node_modules/volar-service-css": { - "version": "0.0.70", - "resolved": "https://registry.npmjs.org/volar-service-css/-/volar-service-css-0.0.70.tgz", - "integrity": "sha512-K1qyOvBpE3rzdAv3e4/6Rv5yizrYPy5R/ne3IWCAzLBuMO4qBMV3kSqWzj6KUVe6S0AnN6wxF7cRkiaKfYMYJw==", + "version": "0.0.71", + "resolved": "https://registry.npmjs.org/volar-service-css/-/volar-service-css-0.0.71.tgz", + "integrity": "sha512-wRRFt9BpjMKCazcgOh67MSjUjiWUCAh99DyYSDIOTuxaRjEtDC7PpB0k1Y1wbJIW/pVtMUSVbpPo3UGSm0Byxw==", "dev": true, "license": "MIT", "dependencies": { @@ -11145,9 +10924,9 @@ } }, "node_modules/volar-service-emmet": { - "version": "0.0.70", - "resolved": "https://registry.npmjs.org/volar-service-emmet/-/volar-service-emmet-0.0.70.tgz", - "integrity": "sha512-xi5bC4m/VyE3zy/n2CXspKeDZs3qA41tHLTw275/7dNWM/RqE2z3BnDICQybHIVp/6G1iOQj5c1qXMgQC08TNg==", + "version": "0.0.71", + "resolved": "https://registry.npmjs.org/volar-service-emmet/-/volar-service-emmet-0.0.71.tgz", + "integrity": "sha512-zqjzt6bN95e3CUstBm0PBFAJnrfz0ZAARka87fart46/gNCLLuP3Vujy8V/J8HEziTFLnfkgIASLFYPUhonJcA==", "dev": true, "license": "MIT", "dependencies": { @@ -11166,9 +10945,9 @@ } }, "node_modules/volar-service-html": { - "version": "0.0.70", - "resolved": "https://registry.npmjs.org/volar-service-html/-/volar-service-html-0.0.70.tgz", - "integrity": "sha512-eR6vCgMdmYAo4n+gcT7DSyBQbwB8S3HZZvSagTf0sxNaD4WppMCFfpqWnkrlGStPKMZvMiejRRVmqsX9dYcTvQ==", + "version": "0.0.71", + "resolved": "https://registry.npmjs.org/volar-service-html/-/volar-service-html-0.0.71.tgz", + "integrity": "sha512-e8tHPhgQ7ooLfudAEIku+kgd9pWkq3SSz8RbnQDI1+Eb8wbenkLGHqoirLqz5ORLV6wIMr2Iv08RWBG5eOcgpw==", "dev": true, "license": "MIT", "dependencies": { @@ -11186,9 +10965,9 @@ } }, "node_modules/volar-service-prettier": { - "version": "0.0.70", - "resolved": "https://registry.npmjs.org/volar-service-prettier/-/volar-service-prettier-0.0.70.tgz", - "integrity": "sha512-Z6BCFSpGVCd8BPAsZ785Kce1BGlWd5ODqmqZGVuB14MJvrR4+CYz6cDy4F+igmE1gMifqfvMhdgT8Aud4M5ngg==", + "version": "0.0.71", + "resolved": "https://registry.npmjs.org/volar-service-prettier/-/volar-service-prettier-0.0.71.tgz", + "integrity": "sha512-Rz7JVH3qD108UCdmIEiZvOBNljMt2nLFdbN8AXcDfn7xD9F5I2aCIsDVqBbXw21PsnxG0b7MfwtNF+zPS/NKUg==", "dev": true, "license": "MIT", "dependencies": { @@ -11208,9 +10987,9 @@ } }, "node_modules/volar-service-typescript": { - "version": "0.0.70", - "resolved": "https://registry.npmjs.org/volar-service-typescript/-/volar-service-typescript-0.0.70.tgz", - "integrity": "sha512-l46Bx4cokkUedTd74ojO5H/zqHZJ8SUuyZ0IB8JN4jfRqUM3bQFBHoOwlZCyZmOeO0A3RQNkMnFclxO4c++gsg==", + "version": "0.0.71", + "resolved": "https://registry.npmjs.org/volar-service-typescript/-/volar-service-typescript-0.0.71.tgz", + "integrity": "sha512-yTtM/BVT6hoyEYnDtaCyAtNhdNeS/mhTTABlBOdw3NNiRBUin3IznFJpgfjer4c6RYopiPjjQjc9VFhxVl1mLw==", "dev": true, "license": "MIT", "dependencies": { @@ -11231,9 +11010,9 @@ } }, "node_modules/volar-service-typescript-twoslash-queries": { - "version": "0.0.70", - "resolved": "https://registry.npmjs.org/volar-service-typescript-twoslash-queries/-/volar-service-typescript-twoslash-queries-0.0.70.tgz", - "integrity": "sha512-IdD13Z9N2Bu8EM6CM0fDV1E69olEYGHDU25X51YXmq8Y0CmJ2LNj6gOiBJgpS5JGUqFzECVhMNBW7R0sPdRTMQ==", + "version": "0.0.71", + "resolved": "https://registry.npmjs.org/volar-service-typescript-twoslash-queries/-/volar-service-typescript-twoslash-queries-0.0.71.tgz", + "integrity": "sha512-9K2k72s4n7rV9s4bX0MyjbX9iBribvKZbBJKuEmTCZfeWJXs6Yh7bGpY4eoc7UufAjvpheBqwyZCOIPBvxCv0A==", "dev": true, "license": "MIT", "dependencies": { @@ -11249,14 +11028,14 @@ } }, "node_modules/volar-service-yaml": { - "version": "0.0.70", - "resolved": "https://registry.npmjs.org/volar-service-yaml/-/volar-service-yaml-0.0.70.tgz", - "integrity": "sha512-0c8bXDBeoATF9F6iPIlOuYTuZAC4c+yi0siQo920u7eiBJk8oQmUmg9cDUbR4+Gl++bvGP4plj3fErbJuPqdcQ==", + "version": "0.0.71", + "resolved": "https://registry.npmjs.org/volar-service-yaml/-/volar-service-yaml-0.0.71.tgz", + "integrity": "sha512-qYGWGuVpUTnZGu5P/CR4KLK4aIR8RrcVnmfZ2eRcj9q/I8VZCoC5yy9FtEvfNvnDp4MU17yhdJcvpQPIqhJS2Q==", "dev": true, "license": "MIT", "dependencies": { "vscode-uri": "^3.0.8", - "yaml-language-server": "~1.20.0" + "yaml-language-server": "~1.23.0" }, "peerDependencies": { "@volar/language-service": "~2.4.0" @@ -11280,6 +11059,13 @@ "vscode-uri": "^3.1.0" } }, + "node_modules/vscode-css-languageservice/node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "dev": true, + "license": "MIT" + }, "node_modules/vscode-html-languageservice": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-5.6.2.tgz", @@ -11311,9 +11097,9 @@ } }, "node_modules/vscode-jsonrpc": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", - "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.1.tgz", + "integrity": "sha512-rfuA6T75H6m5EkbhtEPzre9pT0HPcDI2MMy4+nPFIBks5J8JBAUHD4tRYSgaBOijIEC7SRkC1kKyXTLqbmh9jw==", "dev": true, "license": "MIT", "engines": { @@ -11334,14 +11120,14 @@ } }, "node_modules/vscode-languageserver-protocol": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", - "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "version": "3.18.2", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.18.2.tgz", + "integrity": "sha512-XRyDbT0Pp3sSNti3JmxVEUMySWCSi1hhM+/KUlCy1hV1zmrqpM1OwO12EAki8blhmLuIMpaJrYbo0OzGVfK2Qg==", "dev": true, "license": "MIT", "dependencies": { - "vscode-jsonrpc": "8.2.0", - "vscode-languageserver-types": "3.17.5" + "vscode-jsonrpc": "9.0.1", + "vscode-languageserver-types": "3.18.0" } }, "node_modules/vscode-languageserver-textdocument": { @@ -11352,6 +11138,34 @@ "license": "MIT" }, "node_modules/vscode-languageserver-types": { + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.18.0.tgz", + "integrity": "sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-languageserver/node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver/node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver/node_modules/vscode-languageserver-types": { "version": "3.17.5", "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", @@ -11398,37 +11212,59 @@ "node": ">= 8" } }, - "node_modules/which-pm-runs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", - "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==", + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, "engines": { - "node": ">=4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.0.tgz", - "integrity": "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==", + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.2.3", - "string-width": "^8.2.0", - "strip-ansi": "^7.1.2" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=20" + "node": ">=18" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/xml-naming": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", - "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz", + "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==", "funding": [ { "type": "github", @@ -11472,23 +11308,24 @@ } }, "node_modules/yaml-language-server": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/yaml-language-server/-/yaml-language-server-1.20.0.tgz", - "integrity": "sha512-qhjK/bzSRZ6HtTvgeFvjNPJGWdZ0+x5NREV/9XZWFjIGezew2b4r5JPy66IfOhd5OA7KeFwk1JfmEbnTvev0cA==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/yaml-language-server/-/yaml-language-server-1.23.0.tgz", + "integrity": "sha512-3qVyCOexLCWw06PQa5kRPwvMWMZ/eZeCRWUvgD6a0OkqL/4iCnxy2WumbWifa937Uo5xhyWJ0uxlU39ljhNh7A==", "dev": true, "license": "MIT", "dependencies": { "@vscode/l10n": "^0.0.18", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", - "prettier": "^3.5.0", + "ajv-i18n": "^4.2.0", + "prettier": "^3.8.1", "request-light": "^0.5.7", "vscode-json-languageservice": "4.1.8", "vscode-languageserver": "^9.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-uri": "^3.0.2", - "yaml": "2.7.1" + "yaml": "2.8.3" }, "bin": { "yaml-language-server": "bin/yaml-language-server" @@ -11502,35 +11339,37 @@ "license": "MIT" }, "node_modules/yaml-language-server/node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.1.0.tgz", + "integrity": "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==", "dev": true, "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^8.2.1", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { @@ -11542,71 +11381,6 @@ "node": "^20.19.0 || ^22.12.0 || >=23" } }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/yocto-queue": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", @@ -11620,9 +11394,9 @@ } }, "node_modules/yoctocolors": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", - "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz", + "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==", "dev": true, "license": "MIT", "engines": { @@ -11633,9 +11407,9 @@ } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 9c977e8..769f3c4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "textrefs", "type": "module", - "version": "0.0.1", + "version": "0.1.0", "license": "AGPL-3.0-or-later", "scripts": { "dev": "astro dev", @@ -12,13 +12,14 @@ "astro": "astro", "check": "astro check", "check:fast": "TEXTREFS_REGISTRY_FIXTURE=1 astro check", + "test": "tsx --test scripts/*.test.ts src/lib/*.test.ts", "compile:data": "tsx scripts/compile.ts", "build:data": "npm run compile:data && npm run validate:data", "validate:data": "tsx scripts/validate-data.ts", "format": "prettier --write .", "format:check": "prettier --check .", - "verify": "npm run format:check && npm run check && npm run build", - "verify:fast": "npm run format:check && npm run check:fast && npm run build:fast", + "verify": "npm run format:check && npm run check && npm test && npm run build", + "verify:fast": "npm run format:check && npm run check:fast && npm test && npm run build:fast", "check:links": "astro build && lychee --root-dir \"$(pwd)/dist\" --config lychee.toml dist", "changelog": "git-cliff -o CHANGELOG.md", "changelog:unreleased": "git-cliff --unreleased --prepend CHANGELOG.md", @@ -28,30 +29,32 @@ "*.{astro,ts,js,mjs,cjs,css,md,mdx,json,yml,yaml}": "prettier --write" }, "dependencies": { - "@astrojs/starlight": "^0.39.2", - "astro": "^6.3.1", - "astro-mermaid": "^2.0.1", + "@astrojs/sitemap": "^3.7.3", + "@astrojs/starlight": "^0.41.4", + "astro": "^7.1.3", + "astro-mermaid": "^2.1.0", "mermaid": "^11.15.0", - "sharp": "^0.34.5", - "starlight-blog": "^0.26.1", + "sharp": "^0.35.3", + "spdx-license-ids": "^3.0.23", + "starlight-blog": "^0.28.0", "uuid": "^14.0.0", "yaml": "^2.9.0", - "zod": "^3.23.8" + "zod": "^4.4.3" }, "devDependencies": { "@astrojs/check": "^0.9.9", - "@commitlint/cli": "^21.0.1", + "@commitlint/cli": "^21.2.1", "@commitlint/config-conventional": "^21.0.1", "git-cliff": "^2.13.1", "husky": "^9.1.7", - "lint-staged": "^17.0.5", + "lint-staged": "^17.2.0", "pagefind": "^1.5.2", - "prettier": "^3.8.3", + "prettier": "^3.9.6", "prettier-plugin-astro": "^0.14.1", - "starlight-links-validator": "^0.24.0", - "starlight-llms-txt": "^0.10.0", - "starlight-openapi": "^0.25.3", - "tsx": "^4.19.2", + "starlight-links-validator": "^0.25.2", + "starlight-llms-txt": "^0.11.0", + "starlight-openapi": "^0.26.0", + "tsx": "^4.23.1", "typescript": "^6.0.3" } } diff --git a/public/assets/record.css b/public/assets/record.css new file mode 100644 index 0000000..531a03e --- /dev/null +++ b/public/assets/record.css @@ -0,0 +1,139 @@ +/* Shared stylesheet for minimal /id/ record pages. Served unbundled from + /assets/record.css so 39k+ pages reference one cached file instead of + inlining styles per page. */ +:root { + --fg: #1c1e21; + --fg-muted: #59636e; + --bg: #ffffff; + --bg-code: #f0f1f2; + --hairline: #d9dde1; + --accent: #3b5bdb; + --warn: #d97706; + --info: #2563eb; +} +@media (prefers-color-scheme: dark) { + :root { + --fg: #e6e8ea; + --fg-muted: #9aa4af; + --bg: #17181c; + --bg-code: #24262c; + --hairline: #3a3d45; + --accent: #8da2fb; + } +} +* { + box-sizing: border-box; +} +body { + margin: 0; + color: var(--fg); + background: var(--bg); + font: + 1rem/1.6 system-ui, + sans-serif; +} +main { + max-width: 46rem; + margin: 0 auto; + padding: 1rem 1.25rem 3rem; +} +a { + color: var(--accent); +} +h1 { + font-size: 1.6rem; + line-height: 1.25; +} +h2 { + font-size: 1.15rem; + margin-top: 2rem; +} +code { + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + background: var(--bg-code); + padding: 0.05rem 0.3rem; + border-radius: 3px; + font-size: 0.9em; +} +.site-header { + border-bottom: 1px solid var(--hairline); + padding: 0.6rem 1.25rem; + display: flex; + align-items: baseline; + gap: 0.75rem; + flex-wrap: wrap; +} +.site-header a { + font-weight: 700; + text-decoration: none; + color: var(--fg); +} +.site-header .note { + font-size: 0.8rem; + color: var(--fg-muted); +} +.reg-sub { + color: var(--fg-muted); + font-size: 0.95rem; + margin-bottom: 1.5rem; +} +.reg-iri { + display: inline-block; + word-break: break-all; +} +.reg-tag { + display: inline-block; + font-size: 0.72rem; + background: var(--bg-code); + color: var(--fg-muted); + padding: 0.05rem 0.4rem; + border-radius: 0.25rem; + margin-right: 0.4rem; + vertical-align: middle; +} +ul.reg-list { + list-style: none; + padding: 0; +} +ul.reg-list > li { + padding: 0.45rem 0; + border-bottom: 1px solid var(--hairline); +} +ul.reg-list > li:last-child { + border-bottom: 0; +} +.reg-lang-group + .reg-lang-group { + margin-top: 1rem; +} +.reg-lang-label { + font-weight: 600; + font-size: 0.9rem; + color: var(--fg-muted); + margin-bottom: 0.25rem; +} +.reg-empty { + color: var(--fg-muted); + font-style: italic; +} +.banner { + border-left: 4px solid; + background: var(--bg-code); + padding: 0.75rem 1rem; + margin: 1rem 0; + border-radius: 0.25rem; +} +.banner p { + margin: 0.25rem 0; +} +.banner-draft { + border-color: var(--info); +} +.banner-tombstone { + border-color: var(--warn); +} +.cite-text { + margin: 0.5rem 0 0; + padding: 0.6rem 1rem; + border-left: 3px solid var(--accent); + background: var(--bg-code); +} diff --git a/public/contexts/v1.jsonld b/public/contexts/v1.jsonld index 88a37d9..ce16c99 100644 --- a/public/contexts/v1.jsonld +++ b/public/contexts/v1.jsonld @@ -3,6 +3,7 @@ "tr": "https://textrefs.org/ontology#", "skos": "http://www.w3.org/2004/02/skos/core#", "dcterms": "http://purl.org/dc/terms/", + "prov": "http://www.w3.org/ns/prov#", "schema": "https://schema.org/", "xsd": "http://www.w3.org/2001/XMLSchema#", "id": "@id", @@ -13,6 +14,11 @@ "MappingAssertion": "tr:MappingAssertion", "key": "tr:key", "preferred_label": "skos:prefLabel", + "alternative_labels": { + "@id": "skos:altLabel", + "@container": "@set" + }, + "description": "dcterms:description", "creators": "schema:creator", "kind": "tr:creatorKind", "family": "schema:familyName", @@ -24,8 +30,8 @@ }, "work_key": "tr:workKey", "citation_system_key": "tr:citationSystemKey", + "preferred_citation_system_key": "tr:preferredCitationSystemKey", "locator": "tr:locator", - "normalization_version": "tr:normalizationVersion", "status": "tr:status", "source": "dcterms:source", "created": { @@ -37,12 +43,12 @@ "@type": "xsd:date" }, "relation": "tr:relation", - "exactMatch": { - "@id": "skos:exactMatch", + "alternateOf": { + "@id": "prov:alternateOf", "@type": "@id" }, - "closeMatch": { - "@id": "skos:closeMatch", + "isReferencedBy": { + "@id": "dcterms:isReferencedBy", "@type": "@id" }, "subject": { @@ -50,11 +56,14 @@ "@type": "@id" }, "target": "tr:target", - "target_kind": "tr:targetKind", "identifier": { "@id": "tr:identifier", "@type": "@id" }, + "conforms_to": { + "@id": "dcterms:conformsTo", + "@type": "@id" + }, "resolver_targets": "tr:resolverTargets", "provider": "schema:provider", "url": { @@ -64,18 +73,22 @@ "language": "dcterms:language", "edition": "schema:bookEdition", "access": "tr:access", - "license": "dcterms:license", + "license": { + "@id": "dcterms:license", + "@type": "@id" + }, "license_url": { "@id": "dcterms:license", "@type": "@id" }, + "superseded_by": { + "@id": "dcterms:isReplacedBy", + "@type": "@id" + }, "last_checked": { "@id": "tr:lastChecked", "@type": "xsd:date" }, - "locator_regex": "tr:locatorRegex", - "examples": "tr:examples", - "valid": "tr:validExample", - "invalid": "tr:invalidExample" + "locator_regex": "tr:locatorRegex" } } diff --git a/scripts/compile.test.ts b/scripts/compile.test.ts new file mode 100644 index 0000000..dffad98 --- /dev/null +++ b/scripts/compile.test.ts @@ -0,0 +1,936 @@ +// Compiler coverage for the alias grammar and status dependency rules +// introduced by ADR-0005, on top of ADR-0004's two-state lifecycle. +// +// Each case builds a throwaway registry directory and compiles it, so the tests +// exercise the real YAML → record path (source validation included) rather than +// a hand-written CompiledRegistry. +import { test, type TestContext } from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { v5 as uuidv5 } from 'uuid'; +import { createHash } from 'node:crypto'; +import { + compileRegistry, + dumpResources, + describeResource, + type CompiledRegistry, +} from './compile.js'; +import { LanguageTag } from '../standard/schema/common.js'; +import { Work } from '../standard/schema/work.js'; + +type RegistryFiles = { + systems: Record; + works: Record; +}; + +function compileFixture(files: RegistryFiles): CompiledRegistry { + const root = mkdtempSync(join(tmpdir(), 'textrefs-compile-')); + try { + mkdirSync(join(root, 'systems')); + mkdirSync(join(root, 'works')); + for (const [name, body] of Object.entries(files.systems)) { + writeFileSync(join(root, 'systems', `${name}.yaml`), body); + } + for (const [name, body] of Object.entries(files.works)) { + writeFileSync(join(root, 'works', `${name}.yaml`), body); + } + return compileRegistry(root); + } finally { + rmSync(root, { recursive: true, force: true }); + } +} + +/** Compile expecting failure, without the expected diagnostics spamming stdout. */ +function expectCompileError(t: TestContext, files: RegistryFiles): string { + t.mock.method(console, 'error', () => {}); + try { + compileFixture(files); + } catch (e) { + return e instanceof Error ? e.message : String(e); + } + assert.fail('expected compileRegistry to throw'); +} + +const system = ( + key: string, + status = 'active', + regex = '^(?
[1-9][0-9]*)$', +) => ` +key: ${key} +preferred_label: ${key} +description: Test citation system ${key}. +locator_regex: '${regex}' +status: ${status} +created: 2026-01-01 +modified: 2026-01-01 +`; + +const workHeader = (status = 'active') => ` +work: + key: test.work + preferred_label: Test Work + status: ${status} + created: 2026-01-01 + modified: 2026-01-01 +`; + +const twoSystems = { + 'primary-section': system('primary-section'), + 'fallback-section': system('fallback-section'), +}; + +test('a work under two systems mints one bare and two qualified aliases', () => { + const reg = compileFixture({ + systems: twoSystems, + works: { + 'test.work': `${workHeader()} +citation_system: primary-section +references: + - '5' + +additional_systems: + - citation_system: fallback-section + references: + - '5' +`, + }, + }); + + const primary = reg.references.find( + (r) => r.citation_system_key === 'primary-section', + ); + const fallback = reg.references.find( + (r) => r.citation_system_key === 'fallback-section', + ); + assert.ok(primary && fallback, 'both references are compiled'); + assert.notEqual( + primary.id, + fallback.id, + 'the same locator under two systems is two identities', + ); + + // Qualified for both, bare only for the preferred system. + assert.equal(reg.aliases['test.work/primary-section/5'], primary.id); + assert.equal(reg.aliases['test.work/fallback-section/5'], fallback.id); + assert.equal(reg.aliases['test.work/5'], primary.id); + + const workAliases = Object.keys(reg.aliases).filter((a) => + a.startsWith('test.work/'), + ); + assert.deepEqual(workAliases.sort(), [ + 'test.work/5', + 'test.work/fallback-section/5', + 'test.work/primary-section/5', + ]); +}); + +test('declaring the same citation system twice is rejected at parse time', (t) => { + const message = expectCompileError(t, { + systems: twoSystems, + works: { + 'test.work': `${workHeader()} +citation_system: primary-section +references: + - '5' + +additional_systems: + - citation_system: primary-section + references: + - '6' +`, + }, + }); + assert.match(message, /invalid source file/); +}); + +test('a locator containing "/" is rejected before any alias is minted', (t) => { + const message = expectCompileError(t, { + systems: { + 'slashed-section': system( + 'slashed-section', + 'active', + '^(?[0-9]+)/(?[0-9]+)$', + ), + }, + works: { + 'test.work': `${workHeader()} +citation_system: slashed-section +references: + - '1/5' +`, + }, + }); + assert.match(message, /alias grammar/); +}); + +test('an active reference under a draft citation system is rejected', (t) => { + const message = expectCompileError(t, { + systems: { + 'primary-section': system('primary-section'), + 'fallback-section': system('fallback-section', 'draft'), + }, + works: { + 'test.work': `${workHeader()} +citation_system: primary-section +references: + - '5' + +additional_systems: + - citation_system: fallback-section + reference_status: active + references: + - '5' +`, + }, + }); + assert.match(message, /active reference requires an active citation system/); +}); + +test('an active work whose preferred citation system is draft is rejected', (t) => { + const message = expectCompileError(t, { + systems: { 'primary-section': system('primary-section', 'draft') }, + works: { + 'test.work': `${workHeader()} +citation_system: primary-section +`, + }, + }); + assert.match( + message, + /active work requires an active preferred citation system/, + ); +}); + +test('a draft fallback system does not downgrade an active work', () => { + const reg = compileFixture({ + systems: { + 'primary-section': system('primary-section'), + 'fallback-section': system('fallback-section', 'draft'), + }, + works: { + 'test.work': `${workHeader()} +citation_system: primary-section +references: + - '5' + +additional_systems: + - citation_system: fallback-section + references: + - '5' +`, + }, + }); + assert.equal(reg.works[0].status, 'active'); + const fallback = reg.references.find( + (r) => r.citation_system_key === 'fallback-section', + ); + assert.equal(fallback?.status, 'draft'); +}); + +test('an additional system defaults to draft references even on an active work', () => { + const reg = compileFixture({ + systems: twoSystems, + works: { + 'test.work': `${workHeader()} +citation_system: primary-section +references: + - '5' + +additional_systems: + - citation_system: fallback-section + references: + - '5' +`, + }, + }); + const bySystem = Object.fromEntries( + reg.references.map((r) => [r.citation_system_key, r.status]), + ); + assert.equal(bySystem['primary-section'], 'active'); + assert.equal(bySystem['fallback-section'], 'draft'); +}); + +test('every compiled work carries preferred_citation_system_key', () => { + const reg = compileFixture({ + systems: twoSystems, + works: { + 'test.work': `${workHeader()} +citation_system: primary-section +references: + - '5' +`, + }, + }); + for (const work of reg.works) { + assert.equal(work.preferred_citation_system_key, 'primary-section'); + } +}); + +// --- Alternative labels (ADR-0007) ---------------------------------------- + +/** A work header whose `work:` block carries an `alternative_labels:` list. */ +const workWithLabels = (labels: string[]) => ({ + systems: twoSystems, + works: { + 'test.work': ` +work: + key: test.work + preferred_label: Test Work + alternative_labels: +${labels.map((l) => ` - '${l}'`).join('\n')} + status: active + created: 2026-01-01 + modified: 2026-01-01 + +citation_system: primary-section +references: + - '5' +`, + }, +}); + +test('authored alternative labels reach the compiled work in order', () => { + const reg = compileFixture(workWithLabels(['TW', 'Testwerk'])); + const work = reg.works.find((w) => w.key === 'test.work'); + assert.ok(work); + assert.deepEqual(work.alternative_labels, ['TW', 'Testwerk']); +}); + +test('a work without alternative labels omits the key entirely', () => { + const reg = compileFixture({ + systems: twoSystems, + works: { + 'test.work': `${workHeader()} +citation_system: primary-section +references: + - '5' +`, + }, + }); + const work = reg.works.find((w) => w.key === 'test.work'); + assert.ok(work); + assert.ok(!('alternative_labels' in work)); +}); + +test('an empty alternative_labels list is an authoring error', (t) => { + const message = expectCompileError(t, { + systems: twoSystems, + works: { + 'test.work': ` +work: + key: test.work + preferred_label: Test Work + alternative_labels: [] + status: active + created: 2026-01-01 + modified: 2026-01-01 + +citation_system: primary-section +references: + - '5' +`, + }, + }); + assert.match(message, /invalid source file/); +}); + +test('repeating one alternative label within a work is an authoring error', (t) => { + const message = expectCompileError(t, workWithLabels(['TW', 'TW'])); + assert.match(message, /invalid source file/); +}); + +test('an alternative label equal to the preferred label is an authoring error', (t) => { + const message = expectCompileError(t, workWithLabels(['Test Work'])); + assert.match(message, /invalid source file/); +}); + +test('two different works may share one alternative label', () => { + const shared = (key: string, label: string) => ` +work: + key: ${key} + preferred_label: ${label} + alternative_labels: + - 'Ethics' + status: active + created: 2026-01-01 + modified: 2026-01-01 + +citation_system: primary-section +references: + - '5' +`; + const reg = compileFixture({ + systems: twoSystems, + works: { + 'aristotle.ne': shared('aristotle.ne', 'Nicomachean Ethics'), + 'spinoza.ethica': shared('spinoza.ethica', 'Ethica'), + }, + }); + const claimants = reg.works.filter((w) => + (w.alternative_labels ?? []).includes('Ethics'), + ); + assert.equal(claimants.length, 2); +}); + +// An alternative label is not a UUID seed input (ADR-0002), so editing one +// must leave every minted identifier where it was. +test('alternative labels never move a reference identifier', () => { + const withLabels = compileFixture(workWithLabels(['TW'])); + const withOtherLabels = compileFixture(workWithLabels(['Testwerk', 'TW-2'])); + assert.deepEqual( + withLabels.references.map((r) => r.id), + withOtherLabels.references.map((r) => r.id), + ); +}); + +// The rules above are enforced on the published record too, not only on the +// authored YAML — a hand-built record that reaches a consumer must not be able +// to carry an empty list, a repeat, or the preferred label. +const workRecord = (alternative_labels?: string[]) => ({ + id: 'https://textrefs.org/id/work/test.work', + key: 'test.work', + type: 'Work' as const, + preferred_label: 'Test Work', + ...(alternative_labels ? { alternative_labels } : {}), + preferred_citation_system_key: 'primary-section', + status: 'active' as const, + created: '2026-01-01', + modified: '2026-01-01', +}); + +test('the canonical Work schema accepts a well-formed label list', () => { + assert.equal(Work.safeParse(workRecord(['TW', 'Testwerk'])).success, true); + assert.equal(Work.safeParse(workRecord()).success, true); +}); + +test('the canonical Work schema rejects an empty label list', () => { + assert.equal(Work.safeParse(workRecord([])).success, false); +}); + +test('the canonical Work schema rejects a repeated label', () => { + const result = Work.safeParse(workRecord(['TW', 'TW'])); + assert.equal(result.success, false); + assert.match( + result.error?.issues[0]?.message ?? '', + /declared more than once/, + ); +}); + +test('the canonical Work schema rejects a label equal to preferred_label', () => { + const result = Work.safeParse(workRecord(['Test Work'])); + assert.equal(result.success, false); + assert.match( + result.error?.issues[0]?.message ?? '', + /repeats the preferred label/, + ); +}); + +// --- Mapping relation vocabulary (ADR-0006) ------------------------------- + +const mappings = ` +mappings: + - relation: alternateOf + identifier: 'https://www.wikidata.org/entity/Q1' + source: manual-curation + status: active + created: 2026-01-01 + modified: 2026-01-01 + - relation: isReferencedBy + identifier: 'https://en.wikipedia.org/wiki/Test' + source: manual-curation + status: active + created: 2026-01-01 + modified: 2026-01-01 +`; + +const workWithMappings = (extra = '') => ({ + systems: twoSystems, + works: { + 'test.work': `${workHeader()} +citation_system: primary-section +references: + - '5' +${mappings}${extra}`, + }, +}); + +test('the work projection groups every relation onto its own array', () => { + const reg = compileFixture(workWithMappings()); + const work = reg.works.find((w) => w.key === 'test.work'); + assert.ok(work); + assert.deepEqual(work.alternateOf, ['https://www.wikidata.org/entity/Q1']); + assert.deepEqual(work.isReferencedBy, ['https://en.wikipedia.org/wiki/Test']); + assert.equal(reg.mappings.length, 2); +}); + +test('tombstoned mappings are excluded from the projection', () => { + const reg = compileFixture({ + systems: twoSystems, + works: { + 'test.work': `${workHeader()} +citation_system: primary-section +references: + - '5' + +mappings: + - relation: alternateOf + identifier: 'https://www.wikidata.org/entity/Q1' + source: manual-curation + status: withdrawn + created: 2026-01-01 + modified: 2026-01-01 + - relation: isReferencedBy + identifier: 'https://en.wikipedia.org/wiki/Test' + source: manual-curation + status: blocked + created: 2026-01-01 + modified: 2026-01-01 +`, + }, + }); + const work = reg.works.find((w) => w.key === 'test.work'); + assert.ok(work); + assert.equal(work.alternateOf, undefined); + assert.equal(work.isReferencedBy, undefined); + // The reified assertions survive as tombstones; only the projection drops them. + assert.equal(reg.mappings.length, 2); +}); + +test('deprecated mappings are excluded from the projection', () => { + const reg = compileFixture({ + systems: twoSystems, + works: { + 'test.work': `${workHeader()} +citation_system: primary-section +references: + - '5' + +mappings: + - relation: alternateOf + identifier: 'https://www.wikidata.org/entity/Q1' + source: manual-curation + status: deprecated + created: 2026-01-01 + modified: 2026-01-01 + - relation: isReferencedBy + identifier: 'https://en.wikipedia.org/wiki/Test' + source: manual-curation + status: deprecated + created: 2026-01-01 + modified: 2026-01-01 +`, + }, + }); + const work = reg.works.find((w) => w.key === 'test.work'); + assert.ok(work); + // `deprecated` is retained but no longer recommended (specification §12), so + // the unqualified edge must not advertise it (#45). + assert.equal(work.alternateOf, undefined); + assert.equal(work.isReferencedBy, undefined); + // The reified assertions survive; only the projection drops them. + assert.equal(reg.mappings.length, 2); +}); + +test('mapping IRIs are deterministic from [subject, relation, target]', () => { + const reg = compileFixture(workWithMappings()); + // Recomputed independently here, exactly as validate-data.ts and any + // third-party implementation would (identifier-syntax, MappingAssertion seed). + for (const m of reg.mappings) { + const seed = [m.subject, m.relation, m.target.identifier].join('\n'); + assert.equal( + m.id, + `https://textrefs.org/id/mapping/${uuidv5(seed, 'f16bb214-4241-549d-ad41-7b011f02befb')}`, + ); + } + // The relation is *in* the seed, so reclassifying a target re-mints its IRI. + const [alternate, referenced] = reg.mappings; + assert.notEqual(alternate.id, referenced.id); +}); + +test('a relation outside the enum is rejected at parse time', (t) => { + const message = expectCompileError(t, { + systems: twoSystems, + works: { + 'test.work': `${workHeader()} +citation_system: primary-section +references: + - '5' + +mappings: + - relation: closeMatch + identifier: 'https://en.wikipedia.org/wiki/Test' + source: manual-curation + status: active + created: 2026-01-01 + modified: 2026-01-01 +`, + }, + }); + assert.match(message, /test\.work/); +}); + +// --- Resolver URL expansion ------------------------------------------------- +// +// `vars` (this section's main subject) rides on the same skip-and-warn path as +// a missing template variable, so the pre-existing `url` / `url_by` behaviour +// is pinned here too — it had no coverage before. + +const bookChapter = { + 'book-chapter': system( + 'book-chapter', + 'active', + '^(?[1-4]?[A-Za-z]+)\\.(?[1-9][0-9]*)$', + ), +}; + +/** One work, one resolver, two locators — `Gen.1` and `John.3`. */ +const workWithResolver = (resolver: string) => ({ + systems: bookChapter, + works: { + 'test.work': `${workHeader()} +citation_system: book-chapter +resolvers: +${resolver} +references: + - 'Gen.1' + - 'John.3' +`, + }, +}); + +const urlFor = (reg: CompiledRegistry, locator: string) => + reg.references.find((r) => r.locator === locator)?.resolver_targets[0]?.url; + +test('a url template expands the locator capture groups', () => { + const reg = compileFixture( + workWithResolver(` - url: 'https://example.org/{book}/{chapter}'`), + ); + assert.equal(urlFor(reg, 'Gen.1'), 'https://example.org/Gen/1'); + assert.equal(urlFor(reg, 'John.3'), 'https://example.org/John/3'); + assert.equal(reg.warnings, 0); +}); + +test('url_by picks a whole URL by key, and an absent key skips the entry', () => { + const reg = compileFixture( + workWithResolver(` - url_by: + book: + Gen: 'https://example.org/genesis'`), + ); + assert.equal(urlFor(reg, 'Gen.1'), 'https://example.org/genesis'); + assert.equal(urlFor(reg, 'John.3'), undefined, 'John has no url_by entry'); + assert.equal(reg.warnings, 1); +}); + +test('vars translates a locator value into the provider vocabulary', () => { + const reg = compileFixture( + workWithResolver(` - vars: + bookUsfm: + from: book + map: + Gen: GEN + John: JHN + url: 'https://example.org/{bookUsfm}.{chapter}/#{bookUsfm}.{chapter}'`), + ); + assert.equal(urlFor(reg, 'Gen.1'), 'https://example.org/GEN.1/#GEN.1'); + assert.equal(urlFor(reg, 'John.3'), 'https://example.org/JHN.3/#JHN.3'); + assert.equal(reg.warnings, 0); +}); + +test('a hole in a vars map skips the entry and warns rather than emitting a wrong URL', () => { + const reg = compileFixture( + workWithResolver(` - vars: + bookUsfm: + from: book + map: + Gen: GEN + url: 'https://example.org/{bookUsfm}.{chapter}'`), + ); + assert.equal(urlFor(reg, 'Gen.1'), 'https://example.org/GEN.1'); + assert.equal(urlFor(reg, 'John.3'), undefined, 'John is unmapped'); + assert.equal(reg.warnings, 1); +}); + +test('vars composes with url_by', () => { + const reg = compileFixture( + workWithResolver(` - vars: + slug: + from: book + map: + Gen: genesis + John: john + url_by: + slug: + genesis: 'https://example.org/a' + john: 'https://example.org/b'`), + ); + assert.equal(urlFor(reg, 'Gen.1'), 'https://example.org/a'); + assert.equal(urlFor(reg, 'John.3'), 'https://example.org/b'); + assert.equal(reg.warnings, 0); +}); + +test('a vars name that shadows a locator variable is rejected', (t) => { + const message = expectCompileError( + t, + workWithResolver(` - vars: + book: + from: book + map: + Gen: GEN + url: 'https://example.org/{book}'`), + ); + assert.match(message, /shadows a locator-derived variable/); +}); + +// Variable names and locator values are both author-controlled, so every lookup +// on the way to a URL is own-property only. On a plain object these three cases +// would inherit from Object.prototype and expand into a published URL. + +test('a template variable naming an Object.prototype member is absent, not inherited', () => { + const reg = compileFixture( + workWithResolver(` - url: 'https://example.org/{book}/{toString}'`), + ); + assert.equal(urlFor(reg, 'Gen.1'), undefined); + assert.equal(urlFor(reg, 'John.3'), undefined); + assert.equal(reg.warnings, 2); +}); + +test('a locator value naming an Object.prototype member misses the vars map', () => { + const reg = compileFixture({ + systems: bookChapter, + works: { + 'test.work': `${workHeader()} +citation_system: book-chapter +resolvers: + - vars: + bookUsfm: + from: book + map: + Gen: GEN + url: 'https://example.org/{bookUsfm}.{chapter}' +references: + - 'Gen.1' + - 'toString.1' +`, + }, + }); + assert.equal(urlFor(reg, 'Gen.1'), 'https://example.org/GEN.1'); + assert.equal( + urlFor(reg, 'toString.1'), + undefined, + 'toString is not a mapped book', + ); + assert.equal(reg.warnings, 1); +}); + +// --- Resolver licences ------------------------------------------------------- + +test('an SPDX license id becomes the canonical SPDX IRI', () => { + const reg = compileFixture( + workWithResolver(` - url: 'https://example.org/{book}/{chapter}' + license: CC-BY-4.0`), + ); + assert.equal( + reg.references.find((r) => r.locator === 'Gen.1')?.resolver_targets[0] + ?.license, + 'https://spdx.org/licenses/CC-BY-4.0', + ); +}); + +test('a non-SPDX license id fails the build instead of dropping the licence', (t) => { + const logged: string[] = []; + t.mock.method(console, 'error', (...args: unknown[]) => { + logged.push(args.join(' ')); + }); + assert.throws(() => + compileFixture( + workWithResolver(` - url: 'https://example.org/{book}/{chapter}' + license: CC-BY-4`), + ), + ); + assert.match(logged.join('\n'), /not an SPDX license id/); +}); + +test('url_by with more than one selector variable is rejected at parse time', (t) => { + // The thrown message only names the file; the failing rule goes to + // console.error, so capture it rather than assert on any parse error. + const logged: string[] = []; + t.mock.method(console, 'error', (...args: unknown[]) => { + logged.push(args.join(' ')); + }); + assert.throws(() => + compileFixture( + workWithResolver(` - url_by: + book: + Gen: 'https://example.org/genesis' + chapter: + '1': 'https://example.org/one'`), + ), + ); + 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), + [], + ); +}); + +// --- Dump resources (#84) ------------------------------------------------- + +// `workWithMappings` gives an alias table with both kinds of entry: `/cite/` +// aliases targeting reference IRIs, and external identifiers targeting the work +// IRI. +const dumped = () => dumpResources(compileFixture(workWithMappings())); + +const aliasSpec = (specs: ReturnType) => { + const spec = specs.find((s) => s.name === 'aliases'); + assert.ok(spec, 'the dump carries an alias resource'); + return spec; +}; + +test('the dump carries the alias table beside the four JSONL resources', () => { + const specs = dumped(); + assert.deepEqual( + specs.map((s) => s.name), + ['works', 'citation-systems', 'references', 'mappings', 'aliases'], + ); + const spec = aliasSpec(specs); + assert.equal(spec.filename, 'aliases.json'); + assert.equal(spec.format, 'json'); + assert.equal(spec.mediatype, 'application/json'); +}); + +test('the alias resource body is the whole alias table', () => { + const reg = compileFixture(workWithMappings()); + const spec = aliasSpec(dumpResources(reg)); + const table = JSON.parse(spec.body) as Record; + assert.deepEqual(table, reg.aliases); + // A `/cite/` alias resolves to a reference; an external identifier resolves + // to the work. A `://` in the key marks the second kind. + assert.match( + table['test.work/primary-section/5'], + /^https:\/\/textrefs\.org\/id\/ref\//, + ); + assert.equal( + table['https://www.wikidata.org/entity/Q1'], + 'https://textrefs.org/id/work/test.work', + ); +}); + +test('alias keys are sorted, so the body does not depend on visit order', () => { + const keys = Object.keys(JSON.parse(aliasSpec(dumped()).body)); + assert.ok(keys.length > 1); + assert.deepEqual(keys, [...keys].sort()); +}); + +test('the alias body is minified and newline-terminated', () => { + const body = aliasSpec(dumped()).body; + assert.equal(body.split('\n').length, 2); + assert.ok(body.startsWith('{"')); + assert.ok(body.endsWith('}\n')); +}); + +test('every descriptor states the bytes and sha256 of its own body', () => { + for (const spec of dumped()) { + const d = describeResource(spec); + // Recomputed the way a downstream consumer verifies a download. + assert.equal(d.bytes, Buffer.byteLength(spec.body, 'utf8')); + assert.equal( + d.hash, + `sha256:${createHash('sha256').update(spec.body).digest('hex')}`, + ); + } +}); + +test('every descriptor carries the Frictionless fields', () => { + for (const spec of dumped()) { + const d = describeResource(spec); + assert.equal(d.profile, 'data-resource'); + assert.equal(d.encoding, 'utf-8'); + assert.equal(d.path, spec.filename); + assert.equal(d.name, spec.name); + } +}); + +test('the JSONL bodies are unchanged by the alias-resource refactor', () => { + const reg = compileFixture(workWithMappings()); + const specs = dumpResources(reg); + const bodyOf = (name: string) => specs.find((s) => s.name === name)?.body; + assert.equal( + bodyOf('works'), + reg.works.map((r) => JSON.stringify(r)).join('\n') + '\n', + ); + assert.equal( + bodyOf('references'), + reg.references.map((r) => JSON.stringify(r)).join('\n') + '\n', + ); + // An empty record array still yields an empty body, not a bare newline. + assert.equal( + dumpResources({ ...reg, mappings: [] }).find((s) => s.name === 'mappings') + ?.body, + '', + ); +}); diff --git a/scripts/compile.ts b/scripts/compile.ts index 62f96fb..236b54d 100644 --- a/scripts/compile.ts +++ b/scripts/compile.ts @@ -15,48 +15,25 @@ import { CanonicalReference, MappingAssertion, } from '../standard/schema/index.js'; +import { + parseSource, + SystemSource, + WorkSource, + type MappingSource, + type ReferenceRangeSource as ReferenceRange, + type ReferenceSource, + type ResolverEntrySource as ResolverEntry, + SPDX_IDS, + type SystemBlockSource, +} from './source-schema.js'; const REFERENCE_NS = 'b1a3670e-2ac7-544c-a1b9-396e0dc193f7'; const MAPPING_NS = 'f16bb214-4241-549d-ad41-7b011f02befb'; -const NORMALIZATION_VERSION = '1.0.0'; const projectRoot = resolve(process.cwd()); const dataRoot = join(projectRoot, 'data'); const distRoot = join(projectRoot, 'dist'); -type ResolverEntry = { - url?: string; - url_by?: Record>; - provider?: string; - edition?: string; - language?: string; - access?: 'open' | 'paywalled' | 'restricted' | 'unknown'; - license?: string; - license_url?: string; - last_checked?: string; -}; - -type ReferenceSource = - | string - | { locator: string; extra_resolvers?: ResolverEntry[] }; - -type ReferenceRange = - | { kind: 'integer'; from: number; to: number } - | { kind: 'book_line'; counts: number[] } - | { kind: 'book_chapter'; counts: number[] } - | { kind: 'book_chapter_verse'; book: string; counts: number[] } - | { kind: 'chapter_verse'; counts: number[] } - | { - kind: 'bekker'; - page_ranges: Array<[number, number]>; - lines_per_column: number; - } - | { - kind: 'stephanus'; - page_range: [number, number]; - sections?: string[]; - }; - function expandRange(range: ReferenceRange): string[] { switch (range.kind) { case 'integer': { @@ -125,51 +102,6 @@ function expandRange(range: ReferenceRange): string[] { } } -type MappingSource = { - relation: 'exactMatch' | 'closeMatch'; - target_kind?: string; - identifier: string; - source: string; - status: string; - created: string; - modified: string; -}; - -type CreatorSource = - | { kind: 'person'; family: string; given?: string } - | { kind: 'literal'; name: string }; - -type WorkSource = { - work: { - key: string; - preferred_label: string; - status: string; - created: string; - modified: string; - creators?: CreatorSource[]; - }; - citation_system: string; - mappings?: MappingSource[]; - resolvers?: ResolverEntry[]; - references?: ReferenceSource[]; - references_range?: ReferenceRange[]; -}; - -type SystemSource = { - key: string; - preferred_label: string; - normalization_version: string; - locator_regex: string; - examples: { valid: string[]; invalid: string[] }; - status: string; - created: string; - modified: string; - // Per-chapter verse counts for chapter/verse systems. When present, the - // compiler exposes a `verseGlobal` template variable (cumulative 1..N - // across chapters) for resolvers whose anchors use a single running counter. - chapter_sizes?: number[]; -}; - const ROMAN_NUMERALS: Array<[number, string]> = [ [1000, 'M'], [900, 'CM'], @@ -212,7 +144,11 @@ function deriveLocatorVars( ): Record { const re = new RegExp(system.locator_regex); const m = locator.match(re); - const vars: Record = {}; + // Null-prototype: variable names come from named capture groups and their + // values from locators, both author-controlled. On a plain object a name + // like `constructor` or `toString` would resolve up the prototype chain and + // expand into a URL instead of being treated as absent. + const vars: Record = Object.create(null); if (m?.groups) { for (const [k, v] of Object.entries(m.groups)) { if (v === undefined) continue; @@ -256,6 +192,15 @@ function assertValidLocator(locator: string, system: SystemSource): void { `${system.key}: locator "${locator}" does not match locator_regex`, ); } + // The `/cite/` alias grammar distinguishes `{work}/{locator}` from + // `{work}/{system}/{locator}` by segment count alone (ADR-0005). Flat keys + // already exclude `/`; locators must too, or the two forms become + // ambiguous. Checked before any alias is minted. + if (locator.includes('/')) { + throw new Error( + `${system.key}: locator "${locator}" contains "/", which the /cite/ alias grammar cannot represent`, + ); + } } function expandTemplate( @@ -276,10 +221,42 @@ function expandTemplate( return missing ? null : out; } -function buildResolverEntry( +/** + * Bind a resolver's `vars` on top of the locator-derived variables. Each entry + * translates one canonical value into the provider's own spelling — OSIS `John` + * into die-bibel.de's USFM `JHN`. A source value with no entry in the map is + * treated exactly like a missing template variable: the caller skips the + * resolver for this reference and warns, so a hole in a book table shows up in + * the build output instead of silently emitting a wrong URL. + */ +function applyResolverVars( resolver: ResolverEntry, vars: Record, +): Record | null { + if (!resolver.vars) return vars; + const out: Record = Object.assign(Object.create(null), vars); + for (const [name, spec] of Object.entries(resolver.vars)) { + if (Object.hasOwn(vars, name)) { + throw new Error( + `resolver var "${name}" shadows a locator-derived variable; give the mapped value its own name`, + ); + } + const source = vars[spec.from]; + if (source === undefined) return null; + // `map` is parsed YAML, so own-property only: a locator value of + // `toString` must miss the map, not inherit Object.prototype's method. + if (!Object.hasOwn(spec.map, source)) return null; + out[name] = spec.map[source]; + } + return out; +} + +function buildResolverEntry( + resolver: ResolverEntry, + locatorVars: Record, ): Record | null { + const vars = applyResolverVars(resolver, locatorVars); + if (!vars) return null; let url: string | null = null; if (resolver.url) { url = expandTemplate(resolver.url, vars); @@ -289,7 +266,8 @@ function buildResolverEntry( const byVar = byVars[0]; const key = vars[byVar]; if (key === undefined) return null; - url = resolver.url_by[byVar][key] ?? null; + const byMap = resolver.url_by[byVar]; + url = Object.hasOwn(byMap, key) ? (byMap[key] ?? null) : null; } if (!url) return null; const entry: Record = { url }; @@ -297,7 +275,19 @@ function buildResolverEntry( if (resolver.edition !== undefined) entry.edition = resolver.edition; if (resolver.provider !== undefined) entry.provider = resolver.provider; entry.access = resolver.access ?? 'unknown'; - if (resolver.license !== undefined) entry.license = resolver.license; + if (resolver.license !== undefined) { + if (!SPDX_IDS.has(resolver.license)) { + // Unreachable via authored YAML — ResolverEntrySource rejects it at + // parse time. Kept so a future caller that skips the parser cannot + // drop a licence statement silently. + throw new Error( + `license "${resolver.license}" is not an SPDX id (use license_url for non-SPDX terms)`, + ); + } + // Emit the canonical SPDX IRI so dcterms:license has a single + // IRI-typed range in the JSON-LD output. + entry.license = `https://spdx.org/licenses/${resolver.license}`; + } if (resolver.license_url !== undefined) entry.license_url = resolver.license_url; if (resolver.last_checked !== undefined) @@ -309,9 +299,8 @@ function referenceUuid( workKey: string, systemKey: string, locator: string, - normalizationVersion: string, ): string { - const seed = [workKey, systemKey, locator, normalizationVersion].join('\n'); + const seed = [workKey, systemKey, locator].join('\n'); return uuidv5(seed, REFERENCE_NS); } @@ -338,6 +327,90 @@ function setAlias( aliases[alias] = target; } +/** + * Emit every `CanonicalReference` for one work under one citation system, and + * mint its aliases (ADR-0005): a qualified `{work}/{system}/{locator}` for + * every reference, plus the bare `{work}/{locator}` when this block is the + * work's preferred citation system. Returns the number of skipped resolver + * entries. + */ +function emitBlockReferences(opts: { + workKey: string; + block: SystemBlockSource; + system: SystemSource; + status: string; + created: string; + modified: string; + isPreferred: boolean; + outReferences: CanonicalReference[]; + aliases: Record; +}): number { + const { workKey, block, system, isPreferred, outReferences, aliases } = opts; + const systemKey = system.key; + let warnings = 0; + + const expandedRefs: ReferenceSource[] = ( + block.references_range ?? [] + ).flatMap(expandRange); + const seenLocators = new Set(); + const allRefs: ReferenceSource[] = []; + for (const r of [...expandedRefs, ...(block.references ?? [])]) { + const loc = typeof r === 'string' ? r : r.locator; + if (seenLocators.has(loc)) continue; + seenLocators.add(loc); + allRefs.push(r); + } + + for (const refSrc of allRefs) { + const locator = typeof refSrc === 'string' ? refSrc : refSrc.locator; + assertValidLocator(locator, system); + const extraResolvers = + typeof refSrc === 'string' ? [] : (refSrc.extra_resolvers ?? []); + const vars = deriveLocatorVars(locator, system); + const targets: Record[] = []; + for (const resolver of block.resolvers ?? []) { + const entry = buildResolverEntry(resolver, vars); + if (entry) targets.push(entry); + else warnings++; + } + for (const resolver of extraResolvers) { + const entry = buildResolverEntry(resolver, vars); + if (entry) targets.push(entry); + } + const uuid = referenceUuid(workKey, systemKey, locator); + const record = { + id: `https://textrefs.org/id/ref/${uuid}`, + type: 'CanonicalReference' as const, + work_key: workKey, + citation_system_key: systemKey, + locator, + resolver_targets: targets, + status: opts.status, + created: opts.created, + modified: opts.modified, + }; + const parsed = CanonicalReference.safeParse(record); + if (!parsed.success) { + console.error(`✗ ref/${workKey}/${systemKey}/${locator}: invalid`); + for (const issue of parsed.error.issues) { + console.error( + ` ${issue.path.join('.') || '(root)'}: ${issue.message}`, + ); + } + throw new Error(`invalid reference: ${workKey}/${systemKey}/${locator}`); + } + outReferences.push(parsed.data); + // Qualified alias: always. Keyed by the same tuple that seeds the UUID, + // so it can never collide. + setAlias(aliases, `${workKey}/${systemKey}/${locator}`, record.id); + // Bare alias: only for the preferred system. At most one block per work + // is preferred, so this cannot collide either. + if (isPreferred) setAlias(aliases, `${workKey}/${locator}`, record.id); + } + + return warnings; +} + export interface CompiledRegistry { works: Work[]; systems: CitationSystem[]; @@ -347,14 +420,19 @@ export interface CompiledRegistry { warnings: number; } -export function compileRegistry(): CompiledRegistry { +export function compileRegistry(dataRootOverride?: string): CompiledRegistry { + const root = dataRootOverride ?? dataRoot; const systems = new Map(); - for (const f of listYaml(join(dataRoot, 'systems'))) { - const src = parseYaml(readFileSync(f, 'utf8')) as SystemSource; + for (const f of listYaml(join(root, 'systems'))) { + const src = parseSource( + SystemSource, + parseYaml(readFileSync(f, 'utf8')), + basename(f), + ); systems.set(src.key, src); } - const workFiles = listYaml(join(dataRoot, 'works')); + const workFiles = listYaml(join(root, 'works')); const outWorks: Work[] = []; const outSystems: CitationSystem[] = []; @@ -371,12 +449,12 @@ export function compileRegistry(): CompiledRegistry { key, type: 'CitationSystem' as const, preferred_label: src.preferred_label, - normalization_version: src.normalization_version, + description: src.description, locator_regex: src.locator_regex, - examples: src.examples, status: src.status, created: src.created, modified: src.modified, + ...(src.superseded_by ? { superseded_by: src.superseded_by } : {}), }; const parsed = CitationSystem.safeParse(record); if (!parsed.success) { @@ -392,15 +470,28 @@ export function compileRegistry(): CompiledRegistry { } for (const file of workFiles) { - const src = parseYaml(readFileSync(file, 'utf8')) as WorkSource; + const src = parseSource( + WorkSource, + parseYaml(readFileSync(file, 'utf8')), + basename(file), + ); const workKey = src.work.key; const workIri = `https://textrefs.org/id/work/${workKey}`; const systemKey = src.citation_system; - const system = systems.get(systemKey); - if (!system) { - throw new Error( - `${basename(file)}: references unknown citation_system "${systemKey}"`, - ); + + // Direct mapping edges (prov:alternateOf / dcterms:isReferencedBy via + // the context, ADR-0006) projected from the work's mapping assertions, + // in addition to the reified MappingAssertion records below. Keyed off + // the relation enum so adding a relation needs no branch here. + // A retired assertion does not project: the edge carries no status, so + // it would advertise a mapping the registry has taken out of use (#45). + const mappingEdges: Record = { + alternateOf: [], + isReferencedBy: [], + }; + for (const m of src.mappings ?? []) { + if (RETIRED_STATUSES.has(m.status)) continue; + mappingEdges[m.relation].push(m.identifier); } const workRecord = { @@ -408,10 +499,22 @@ export function compileRegistry(): CompiledRegistry { key: workKey, type: 'Work' as const, preferred_label: src.work.preferred_label, + ...(src.work.alternative_labels + ? { alternative_labels: src.work.alternative_labels } + : {}), + // The top-level `citation_system:` block is the preferred one + // (ADR-0005); it is what mints the bare `/cite/{work}/{locator}` alias. + preferred_citation_system_key: systemKey, status: src.work.status, created: src.work.created, modified: src.work.modified, + ...(src.work.superseded_by + ? { superseded_by: src.work.superseded_by } + : {}), ...(src.work.creators ? { creators: src.work.creators } : {}), + ...Object.fromEntries( + Object.entries(mappingEdges).filter(([, targets]) => targets.length), + ), }; const workParsed = Work.safeParse(workRecord); if (!workParsed.success) { @@ -433,10 +536,10 @@ export function compileRegistry(): CompiledRegistry { subject: workIri, relation: mapping.relation, target: { - ...(mapping.target_kind !== undefined && { - target_kind: mapping.target_kind, - }), identifier: mapping.identifier, + ...(mapping.conforms_to !== undefined && { + conforms_to: mapping.conforms_to, + }), }, source: mapping.source, status: mapping.status, @@ -454,68 +557,54 @@ export function compileRegistry(): CompiledRegistry { throw new Error(`invalid mapping: ${uuid}`); } outMappings.push(parsed.data); + // Deliberate under ADR-0006: an `isReferencedBy` target (a page + // *about* the work) stays a lookup alias for it. The alias table is + // a lookup convenience, not an identity claim. setAlias(aliases, mapping.identifier, workIri); } - const explicitRefs: ReferenceSource[] = src.references ?? []; - const expandedRefs: ReferenceSource[] = ( - src.references_range ?? [] - ).flatMap(expandRange); - const seenLocators = new Set(); - const allRefs: ReferenceSource[] = []; - for (const r of [...expandedRefs, ...explicitRefs]) { - const loc = typeof r === 'string' ? r : r.locator; - if (seenLocators.has(loc)) continue; - seenLocators.add(loc); - allRefs.push(r); - } - - for (const refSrc of allRefs) { - const locator = typeof refSrc === 'string' ? refSrc : refSrc.locator; - assertValidLocator(locator, system); - const extraResolvers = - typeof refSrc === 'string' ? [] : (refSrc.extra_resolvers ?? []); - const vars = deriveLocatorVars(locator, system); - const targets: Record[] = []; - for (const resolver of src.resolvers ?? []) { - const entry = buildResolverEntry(resolver, vars); - if (entry) targets.push(entry); - else warnings++; - } - for (const resolver of extraResolvers) { - const entry = buildResolverEntry(resolver, vars); - if (entry) targets.push(entry); + // The preferred block first, then any fallback systems. Each block is + // emitted against its own citation system, resolvers, and status. + const blocks: Array<{ block: SystemBlockSource; isPreferred: boolean }> = [ + { + block: { + citation_system: systemKey, + reference_status: src.reference_status, + resolvers: src.resolvers, + references: src.references, + references_range: src.references_range, + }, + isPreferred: true, + }, + ...(src.additional_systems ?? []).map((block) => ({ + block, + isPreferred: false, + })), + ]; + + for (const { block, isPreferred } of blocks) { + const system = systems.get(block.citation_system); + if (!system) { + throw new Error( + `${basename(file)}: references unknown citation_system "${block.citation_system}"`, + ); } - const uuid = referenceUuid( + warnings += emitBlockReferences({ workKey, - systemKey, - locator, - NORMALIZATION_VERSION, - ); - const record = { - id: `https://textrefs.org/id/ref/${uuid}`, - type: 'CanonicalReference' as const, - work_key: workKey, - citation_system_key: systemKey, - locator, - normalization_version: NORMALIZATION_VERSION, - resolver_targets: targets, - status: src.work.status, + block, + system, + // The preferred block inherits the work's status; a fallback block + // defaults to `draft`, never to the work's status, so adding a + // system to an active work never promotes data by inheritance + // (ADR-0005). + status: + block.reference_status ?? (isPreferred ? src.work.status : 'draft'), created: src.work.created, modified: src.work.modified, - }; - const parsed = CanonicalReference.safeParse(record); - if (!parsed.success) { - console.error(`✗ ref/${workKey}/${locator}: invalid`); - for (const issue of parsed.error.issues) { - console.error( - ` ${issue.path.join('.') || '(root)'}: ${issue.message}`, - ); - } - throw new Error(`invalid reference: ${workKey}/${locator}`); - } - outReferences.push(parsed.data); - setAlias(aliases, `${workKey}/${locator}`, record.id); + isPreferred, + outReferences, + aliases, + }); } } @@ -523,7 +612,7 @@ export function compileRegistry(): CompiledRegistry { outReferences.sort((a, b) => a.id.localeCompare(b.id)); outMappings.sort((a, b) => a.id.localeCompare(b.id)); - warnings += enforceTombstoneInvariants({ + warnings += enforceRegistryInvariants({ works: outWorks, systems: outSystems, references: outReferences, @@ -540,20 +629,24 @@ export function compileRegistry(): CompiledRegistry { }; } -type TombstoneRecord = { +type StatusRecord = { id: string; status: string; + superseded_by?: string; }; -const TOMBSTONE_STATUSES = new Set(['withdrawn']); +const TOMBSTONE_STATUSES = new Set(['withdrawn', 'blocked']); +// ADR-0004: deprecated, withdrawn and blocked are the states for records that +// have left active use. +const RETIRED_STATUSES = new Set(['deprecated', 'withdrawn', 'blocked']); -function enforceTombstoneInvariants(reg: { +function enforceRegistryInvariants(reg: { works: Work[]; systems: CitationSystem[]; references: CanonicalReference[]; mappings: MappingAssertion[]; }): number { - const all: TombstoneRecord[] = [ + const all: StatusRecord[] = [ ...reg.works, ...reg.systems, ...reg.references, @@ -567,29 +660,95 @@ function enforceTombstoneInvariants(reg: { const errors: string[] = []; - // Active CanonicalReferences MUST NOT point at tombstoned work/system — - // those break resolution. MappingAssertions are intentionally exempt: - // successor links are carried by active exactMatch mappings whose subject - // is the withdrawn IRI and whose target is the active successor. - const isActive = (r: TombstoneRecord) => !TOMBSTONE_STATUSES.has(r.status); + // superseded_by carries the successor of a record that has left active use. + // A record still in use has none. + for (const r of all) { + if (r.superseded_by !== undefined && !RETIRED_STATUSES.has(r.status)) { + errors.push( + `${r.id}: superseded_by is only allowed on deprecated/withdrawn/blocked records (status: ${r.status})`, + ); + } + } + + // A reference that is not itself a tombstone MUST NOT point at a tombstoned + // work or system — those break resolution. Successor links are carried by + // the tombstoned record's own superseded_by field (dcterms:isReplacedBy), + // not by MappingAssertions, which are reserved for work-level equivalence. for (const ref of reg.references) { - if (!isActive(ref)) continue; + if (TOMBSTONE_STATUSES.has(ref.status)) continue; const workIri = `https://textrefs.org/id/work/${ref.work_key}`; const systemIri = `https://textrefs.org/id/system/${ref.citation_system_key}`; if (tombstoneIris.has(workIri)) errors.push( - `${ref.id}: active reference points at tombstoned work ${workIri}`, + `${ref.id}: live reference points at tombstoned work ${workIri}`, ); if (tombstoneIris.has(systemIri)) errors.push( - `${ref.id}: active reference points at tombstoned system ${systemIri}`, + `${ref.id}: live reference points at tombstoned system ${systemIri}`, + ); + } + + // Status dependency rules — ADR-0004's general rule (an active record MUST + // NOT depend on a draft one), specialised by ADR-0005: + // + // an active reference requires an active work AND an active citation + // system for its own citation_system_key; + // an active work requires an active preferred citation system. + // + // A work MAY additionally carry draft fallback systems and draft references + // without being downgraded — the status of a fallback never touches the work. + const statusByIri = new Map(); + for (const r of [...reg.works, ...reg.systems]) + statusByIri.set(r.id, r.status); + const systemKeys = new Set(reg.systems.map((s) => s.key)); + + for (const work of reg.works) { + const key = work.preferred_citation_system_key; + if (!systemKeys.has(key)) { + errors.push( + `${work.id}: preferred_citation_system_key "${key}" is not a known CitationSystem`, + ); + continue; + } + const preferredIri = `https://textrefs.org/id/system/${key}`; + const preferredStatus = statusByIri.get(preferredIri); + if (work.status === 'active' && preferredStatus !== 'active') + errors.push( + `${work.id}: active work requires an active preferred citation system, but ${preferredIri} is ${preferredStatus}`, + ); + } + + for (const ref of reg.references) { + if (ref.status !== 'active') continue; + const workIri = `https://textrefs.org/id/work/${ref.work_key}`; + const systemIri = `https://textrefs.org/id/system/${ref.citation_system_key}`; + const workStatus = statusByIri.get(workIri); + const systemStatus = statusByIri.get(systemIri); + if (workStatus !== 'active') + errors.push( + `${ref.id}: active reference requires an active work, but ${workIri} is ${workStatus ?? 'missing'}`, + ); + if (systemStatus !== 'active') + errors.push( + `${ref.id}: active reference requires an active citation system, but ${systemIri} is ${systemStatus ?? 'missing'}`, + ); + } + + for (const mapping of reg.mappings) { + if (mapping.status !== 'active') continue; + const subjectStatus = statusByIri.get(mapping.subject); + if (subjectStatus !== 'active') + errors.push( + `${mapping.id}: active mapping requires an active subject work, but ${mapping.subject} is ${subjectStatus ?? 'missing'}`, ); } if (errors.length > 0) { for (const e of errors) console.error(`✗ ${e}`); throw new Error( - `${errors.length} tombstone invariant violation(s); fix the offending records`, + `${errors.length} registry invariant violation(s); fix the offending records:\n${errors + .map((e) => ` - ${e}`) + .join('\n')}`, ); } return 0; @@ -601,53 +760,109 @@ function readPackageVersion(): string { return pkg.version; } -function writeDump(registry: CompiledRegistry, version: string): void { - const dumpDir = join(distRoot, 'dump'); - mkdirSync(dumpDir, { recursive: true }); +type ResourceSpec = { + name: string; + filename: string; + format: string; + mediatype: string; + /** + * The pre-serialised body. The dump is no longer JSONL-only, so each + * resource carries its own bytes rather than a record array. + */ + body: string; +}; - type ResourceSpec = { - name: string; - filename: string; - records: ReadonlyArray; - }; +interface ResourceDescriptor { + name: string; + path: string; + profile: 'data-resource'; + format: string; + mediatype: string; + encoding: 'utf-8'; + bytes: number; + hash: string; +} - const specs: ResourceSpec[] = [ - { name: 'works', filename: 'works.jsonl', records: registry.works }, - { - name: 'citation-systems', - filename: 'citation-systems.jsonl', - records: registry.systems, - }, - { - name: 'references', - filename: 'references.jsonl', - records: registry.references, - }, +function jsonlBody(records: ReadonlyArray): string { + return records.length === 0 + ? '' + : records.map((r) => JSON.stringify(r)).join('\n') + '\n'; +} + +/** + * Every `/dump/` resource body, in descriptor order. Pure — `writeDump` does + * the I/O — so the resource set, its media types, and its bodies are testable + * without a filesystem. + */ +export function dumpResources(registry: CompiledRegistry): ResourceSpec[] { + const jsonl = ( + name: string, + filename: string, + records: ReadonlyArray, + ): ResourceSpec => ({ + name, + filename, + format: 'jsonl', + mediatype: 'application/x-ndjson', + body: jsonlBody(records), + }); + + // The complete alias table (#84). Two kinds of entry share it: a `/cite/` + // alias targeting a reference IRI, and an external mapping identifier + // targeting a work IRI. Values stay full IRIs so a consumer can tell the two + // apart; a `://` in the key marks the second kind. + // + // Keys are sorted by code unit, so the body — and therefore its sha256 — + // depends on the registry content alone, never on the order the compiler + // happened to visit the work files in. No indentation: the body is ~13 MB. + const aliasBody = + JSON.stringify( + Object.fromEntries( + Object.entries(registry.aliases).sort(([a], [b]) => + a < b ? -1 : a > b ? 1 : 0, + ), + ), + ) + '\n'; + + return [ + jsonl('works', 'works.jsonl', registry.works), + jsonl('citation-systems', 'citation-systems.jsonl', registry.systems), + jsonl('references', 'references.jsonl', registry.references), + jsonl('mappings', 'mappings.jsonl', registry.mappings), { - name: 'mappings', - filename: 'mappings.jsonl', - records: registry.mappings, + name: 'aliases', + filename: 'aliases.json', + format: 'json', + mediatype: 'application/json', + body: aliasBody, }, ]; +} - const resources = specs.map((spec) => { - const body = - spec.records.length === 0 - ? '' - : spec.records.map((r) => JSON.stringify(r)).join('\n') + '\n'; - const bytes = Buffer.byteLength(body, 'utf8'); - const hash = createHash('sha256').update(body).digest('hex'); - writeFileSync(join(dumpDir, spec.filename), body); - return { - name: spec.name, - path: spec.filename, - profile: 'data-resource', - format: 'jsonl', - mediatype: 'application/x-ndjson', - encoding: 'utf-8', - bytes, - hash: `sha256:${hash}`, - }; +/** + * The Frictionless descriptor of one resource body: the byte count and the + * `sha256:` hash that a consumer recomputes to verify a download. + */ +export function describeResource(spec: ResourceSpec): ResourceDescriptor { + return { + name: spec.name, + path: spec.filename, + profile: 'data-resource', + format: spec.format, + mediatype: spec.mediatype, + encoding: 'utf-8', + bytes: Buffer.byteLength(spec.body, 'utf8'), + hash: `sha256:${createHash('sha256').update(spec.body).digest('hex')}`, + }; +} + +function writeDump(registry: CompiledRegistry, version: string): void { + const dumpDir = join(distRoot, 'dump'); + mkdirSync(dumpDir, { recursive: true }); + + const resources = dumpResources(registry).map((spec) => { + writeFileSync(join(dumpDir, spec.filename), spec.body); + return describeResource(spec); }); const datapackage = { diff --git a/scripts/source-schema.ts b/scripts/source-schema.ts new file mode 100644 index 0000000..d9cb93a --- /dev/null +++ b/scripts/source-schema.ts @@ -0,0 +1,254 @@ +// Runtime schemas for the registry's YAML *source* files (`data/works/*.yaml`, +// `data/systems/*.yaml`). These are compiler inputs, not published records, so +// they live here rather than in `standard/schema/`. The published shapes stay +// authoritative — see `standard/schema/` — but a typo in source YAML used to +// slip through an unchecked `as WorkSource` cast and surface much later, or not +// at all. Objects are strict: an unknown key is an authoring error. +import { createRequire } from 'node:module'; +import { z } from 'zod'; +import { + FlatKey, + IsoDate, + LanguageTag, + Status, +} from '../standard/schema/common.js'; + +const require = createRequire(import.meta.url); +const spdxLicenseIds: string[] = require('spdx-license-ids'); +const spdxDeprecatedIds: string[] = require('spdx-license-ids/deprecated'); + +/** Current and deprecated SPDX identifiers, the only accepted `license` values. */ +export const SPDX_IDS = new Set([ + ...spdxLicenseIds, + ...spdxDeprecatedIds, +]); + +export const ResolverEntrySource = z + .strictObject({ + url: z.string().min(1).optional(), + url_by: z.record(z.string(), z.record(z.string(), z.string())).optional(), + // Provider-specific spellings of a locator variable. The canonical + // vocabulary stays in the locator (e.g. the OSIS book code `John`); a + // provider that names the same book differently (die-bibel.de's USFM + // `JHN`) declares the translation here rather than forcing a second + // citation system into existence. Compile-time only — nothing new + // reaches the published record, just a different expanded URL. + vars: z + .record( + z.string().min(1), + z.strictObject({ + from: z.string().min(1), + map: z.record(z.string().min(1), z.string().min(1)), + }), + ) + .optional(), + provider: z.string().min(1).optional(), + edition: z.string().min(1).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 + // of the published record silently. A typo must fail the build, not + // remove the licence statement. + license: z + .string() + .min(1) + .refine((id) => SPDX_IDS.has(id), { + message: 'not an SPDX license id (use license_url for non-SPDX terms)', + }) + .optional(), + license_url: z.string().min(1).optional(), + last_checked: IsoDate.optional(), + }) + .refine((r) => r.url !== undefined || r.url_by !== undefined, { + message: 'resolver needs either url or url_by', + }) + // `url_by` selects a whole URL by the value of exactly one variable. More + // than one selector has no defined meaning, and the compiler would skip the + // entry per reference — an authoring typo would surface as silently missing + // resolver targets rather than a failed build. + .refine((r) => r.url_by === undefined || Object.keys(r.url_by).length === 1, { + message: 'url_by takes exactly one selector variable', + }); + +export type ResolverEntrySource = z.infer; + +export const ReferenceSource = z.union([ + z.string().min(1), + z.strictObject({ + locator: z.string().min(1), + extra_resolvers: z.array(ResolverEntrySource).optional(), + }), +]); + +export type ReferenceSource = z.infer; + +const Counts = z.array(z.number().int().positive()); +const PageRange = z.tuple([ + z.number().int().positive(), + z.number().int().positive(), +]); + +export const ReferenceRangeSource = z.discriminatedUnion('kind', [ + z.strictObject({ + kind: z.literal('integer'), + from: z.number().int(), + to: z.number().int(), + }), + z.strictObject({ kind: z.literal('book_line'), counts: Counts }), + z.strictObject({ kind: z.literal('book_chapter'), counts: Counts }), + z.strictObject({ + kind: z.literal('book_chapter_verse'), + book: z.string().min(1), + counts: Counts, + }), + z.strictObject({ kind: z.literal('chapter_verse'), counts: Counts }), + z.strictObject({ + kind: z.literal('bekker'), + page_ranges: z.array(PageRange), + lines_per_column: z.number().int().positive(), + }), + z.strictObject({ + kind: z.literal('stephanus'), + page_range: PageRange, + sections: z.array(z.string().min(1)).optional(), + }), +]); + +export type ReferenceRangeSource = z.infer; + +export const MappingSource = z.strictObject({ + // ADR-0006: chosen by what the target *is*, never by confidence. + // alternateOf — another entity denoting the same work. + // isReferencedBy — a document or page about the work. + relation: z.enum(['alternateOf', 'isReferencedBy']), + identifier: z.string().min(1), + conforms_to: z + .union([z.string().min(1), z.array(z.string().min(1))]) + .optional(), + source: z.string().min(1), + status: Status, + created: IsoDate, + modified: IsoDate, +}); + +export type MappingSource = z.infer; + +export const CreatorSource = z.discriminatedUnion('kind', [ + z.strictObject({ + kind: z.literal('person'), + family: z.string().min(1), + given: z.string().min(1).optional(), + }), + z.strictObject({ kind: z.literal('literal'), name: z.string().min(1) }), +]); + +export type CreatorSource = z.infer; + +// One citation system's worth of authoring: which system, what status its +// references carry, and the resolvers and locators scoped to it. Resolver URL +// templates belong to the block because their variables come from that +// system's `locator_regex` capture groups (ADR-0005). +const systemBlockFields = { + citation_system: FlatKey, + reference_status: Status.optional(), + resolvers: z.array(ResolverEntrySource).optional(), + references: z.array(ReferenceSource).optional(), + references_range: z.array(ReferenceRangeSource).optional(), +}; + +export const AdditionalSystemSource = z.strictObject(systemBlockFields); + +export type SystemBlockSource = z.infer; + +export const WorkSource = z + .strictObject({ + work: z.strictObject({ + key: FlatKey, + preferred_label: z.string().min(1), + // Abbreviations, translated titles, and established short forms. + // Omit the key rather than authoring an empty list. + alternative_labels: z.array(z.string().min(1)).min(1).optional(), + status: Status, + created: IsoDate, + modified: IsoDate, + superseded_by: z.string().min(1).optional(), + creators: z.array(CreatorSource).optional(), + }), + // The top-level block is the work's PREFERRED citation system: the one + // whose references also get the bare `/cite/{work}/{locator}` alias. + ...systemBlockFields, + mappings: z.array(MappingSource).optional(), + // Fallback systems. Each is reviewed on its own and defaults to `draft` + // references, so adding one never promotes data by inheritance. + additional_systems: z.array(AdditionalSystemSource).optional(), + }) + .superRefine((src, ctx) => { + // Alternative labels are unique inside one work, and none of them + // repeats the preferred label. Two different works MAY share a label — + // "Ethics" fits Aristotle and Spinoza — so this check stays local + // (ADR-0007). + const seenLabels = new Set(); + (src.work.alternative_labels ?? []).forEach((label, i) => { + if (label === src.work.preferred_label) { + ctx.addIssue({ + code: 'custom', + message: `alternative label "${label}" repeats the preferred label`, + path: ['work', 'alternative_labels', i], + }); + } else if (seenLabels.has(label)) { + ctx.addIssue({ + code: 'custom', + message: `alternative label "${label}" is declared more than once for this work`, + path: ['work', 'alternative_labels', i], + }); + } + seenLabels.add(label); + }); + + const seen = new Set([src.citation_system]); + (src.additional_systems ?? []).forEach((block, i) => { + if (seen.has(block.citation_system)) { + ctx.addIssue({ + code: 'custom', + message: `citation system "${block.citation_system}" is declared more than once for this work`, + path: ['additional_systems', i, 'citation_system'], + }); + } + seen.add(block.citation_system); + }); + }); + +export type WorkSource = z.infer; + +export const SystemSource = z.strictObject({ + key: FlatKey, + preferred_label: z.string().min(1), + description: z.string().min(1), + locator_regex: z.string().min(1), + status: Status, + created: IsoDate, + modified: IsoDate, + superseded_by: z.string().min(1).optional(), + // Per-chapter verse counts for chapter/verse systems. When present, the + // compiler exposes a `verseGlobal` template variable (cumulative 1..N + // across chapters) for resolvers whose anchors use a single running counter. + chapter_sizes: z.array(z.number().int().positive()).optional(), +}); + +export type SystemSource = z.infer; + +/** Parse one source file, reporting the file name and field path on failure. */ +export function parseSource( + schema: T, + raw: unknown, + file: string, +): z.infer { + const parsed = schema.safeParse(raw); + if (parsed.success) return parsed.data; + console.error(`✗ ${file}: invalid source`); + for (const issue of parsed.error.issues) { + console.error(` ${issue.path.join('.') || '(root)'}: ${issue.message}`); + } + throw new Error(`invalid source file: ${file}`); +} diff --git a/scripts/validate-data.ts b/scripts/validate-data.ts index fa9ed91..5813a2d 100644 --- a/scripts/validate-data.ts +++ b/scripts/validate-data.ts @@ -22,11 +22,12 @@ let checked = 0; function reportIssue( label: string, - issues: { path: (string | number)[]; message: string }[], + issues: readonly { path: readonly PropertyKey[]; message: string }[], ): void { console.error(`✗ ${label}:`); for (const issue of issues) { - console.error(` ${issue.path.join('.') || '(root)'}: ${issue.message}`); + const path = issue.path.map((p) => String(p)).join('.'); + console.error(` ${path || '(root)'}: ${issue.message}`); } failed++; } @@ -50,12 +51,7 @@ for (const ref of registry.references) { reportIssue(`ref/${ref.work_key}/${ref.locator}`, r.error.issues); continue; } - const seed = [ - ref.work_key, - ref.citation_system_key, - ref.locator, - ref.normalization_version, - ].join('\n'); + const seed = [ref.work_key, ref.citation_system_key, ref.locator].join('\n'); const expected = `https://textrefs.org/id/ref/${uuidv5(seed, REFERENCE_NS)}`; if (ref.id !== expected) { console.error( diff --git a/src/components/AliasList.astro b/src/components/AliasList.astro index 1f3dfd5..e479755 100644 --- a/src/components/AliasList.astro +++ b/src/components/AliasList.astro @@ -21,7 +21,7 @@ const aliasUrls = aliases.map((alias) => {aliasUrls.map((aliasUrl) => (
  • diff --git a/src/components/Tombstone.astro b/src/components/Tombstone.astro index 34174ee..0a3cd14 100644 --- a/src/components/Tombstone.astro +++ b/src/components/Tombstone.astro @@ -1,23 +1,48 @@ --- interface Props { status: string; + supersededBy?: string; } -const { status } = Astro.props; +const { status, supersededBy } = Astro.props; -const isTombstone = status === 'withdrawn'; +const isDraft = status === 'draft'; +const isTombstone = status === 'withdrawn' || status === 'blocked'; --- +{ + isDraft && ( + + ) +} + { isTombstone && ( ) } @@ -36,4 +61,17 @@ const isTombstone = status === 'withdrawn'; .reg-tombstone p { margin: 0.25rem 0; } + .reg-draft { + border-left: 4px solid var(--sl-color-blue, #2563eb); + background: var(--sl-color-bg-nav, rgba(37, 99, 235, 0.08)); + padding: 0.75rem 1rem; + margin: 1rem 0; + border-radius: 0.25rem; + } + .reg-draft-title { + margin-top: 0; + } + .reg-draft p { + margin: 0.25rem 0; + } diff --git a/src/content.config.ts b/src/content.config.ts index 8a89761..e32b8a0 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -15,14 +15,14 @@ export const Maturity = z.enum([ export const collections = { docs: defineCollection({ loader: docsLoader(), + // `extend` must return a ZodObject: Starlight deep-merges it into its own + // frontmatter shape, so an intersection (`.and()`) is rejected. schema: docsSchema({ extend: (context) => - blogSchema(context).and( - z.object({ - maturity: Maturity.optional(), - banner: z.object({ content: z.string() }).default(UNSTABLE_BANNER), - }), - ), + blogSchema(context).extend({ + maturity: Maturity.optional(), + banner: z.object({ content: z.string() }).default(UNSTABLE_BANNER), + }), }), }), }; diff --git a/src/content/docs/404.mdx b/src/content/docs/404.mdx index 2f38e0b..8dba66b 100644 --- a/src/content/docs/404.mdx +++ b/src/content/docs/404.mdx @@ -3,6 +3,7 @@ title: '404' description: This page could not be found. Help improve TextRefs by reporting broken links or missing content. template: splash editUrl: false +draft: true hero: title: This reference is missing. tagline: If you expected a TextRefs page here, please help us improve the registry and documentation. diff --git a/src/content/docs/association/board.md b/src/content/docs/association/board.md index 286be65..6ba4504 100644 --- a/src/content/docs/association/board.md +++ b/src/content/docs/association/board.md @@ -5,7 +5,7 @@ sidebar: order: 6 --- -The Board is the association's executive organ. Per the [statutes](/association/statutes/#art-11-board), the Board consists of at least three persons serving two-year terms on a volunteer basis ([Art. 15](/association/statutes/#art-15-volunteer-service-expenses-and-compensation)). +The Board is the association's executive organ. Per the [statutes](/association/statutes/#art-11-board), the Board consists of at least two persons serving two-year terms on a volunteer basis ([Art. 15](/association/statutes/#art-15-volunteer-service-expenses-and-compensation)). ## Current members diff --git a/src/content/docs/association/governance.md b/src/content/docs/association/governance.md index fe0c291..acebc85 100644 --- a/src/content/docs/association/governance.md +++ b/src/content/docs/association/governance.md @@ -96,7 +96,8 @@ A technical review is usually sufficient for: - minor metadata corrections; - updates to `last_checked` fields; - adding uncontested aliases; -- documentation corrections without expert implications. +- documentation corrections without expert implications; +- entry of new records as `draft` (merging validated data into the registry). Prerequisites: @@ -118,7 +119,8 @@ An expert review is required for: - contested mapping statements; - conflicting external identifiers; - changes with significant expert or reputational impact; -- status changes to `active`, `deprecated`, `withdrawn`, or `blocked`, where these are subject-matter decisions. +- promotion of records from `draft` to `active` (the point at which the persistence guarantee attaches); +- status changes to `deprecated`, `withdrawn`, or `blocked`, where these are subject-matter decisions. Prerequisites: @@ -143,16 +145,16 @@ The Board decides or confirms: ### 5.1 General record status -- **candidate:** proposed record, not yet fully reviewed; -- **active:** valid and recommended; +- **draft:** work-in-progress record; correctable or retractable without a tombstone; excluded from the persistence guarantee; +- **active:** valid and recommended; permanently identified from this point on; - **deprecated:** no longer recommended, but retained for historical reasons; - **withdrawn:** withdrawn, landing page retained; - **blocked:** blocked on legal, policy, or serious quality grounds. ### 5.2 Mapping status -- **candidate:** proposed mapping; -- **active:** reviewed and recommended mapping; +- **draft:** work-in-progress mapping; correctable or retractable without a tombstone; +- **active:** reviewed and recommended mapping; permanently identified from this point on; - **deprecated:** mapping no longer recommended; - **withdrawn:** withdrawn mapping; - **blocked:** blocked mapping. @@ -161,7 +163,7 @@ Contested mappings remain in the appropriate schema status and carry the documen ### 5.3 Tombstone principle -Published IDs are, as a matter of principle, not hard-deleted. +Active IDs (status `active` or a tombstone status) are, as a matter of principle, not hard-deleted. Records in `draft` status may be retracted without a landing page. In the event of withdrawal, blocking, or deprecation, a landing page is retained with: @@ -176,10 +178,10 @@ In the event of withdrawal, blocking, or deprecation, a landing page is retained 1. Primary TextRefs IDs are independent HTTP URIs. 2. External identifiers such as CTS URNs, Wikidata IDs, DOIs, ARKs, Perseus URLs, or Scaife URLs are not primary TextRefs IDs. -3. Stable and established references may receive deterministic IDs. -4. Provisional or uncertain objects receive generated IDs. +3. All registry records carry deterministic IDs computable from their identity fields. +4. Provisional records are expressed through the `draft` status, which is excluded from the persistence guarantee; their IDs may disappear or change until promotion to `active`. 5. Once published, IDs are not changed merely because labels, titles, aliases, or external mappings are improved. -6. Human-readable citation URLs are aliases and may be redirected, changed, or deprecated; primary IDs remain authoritative. +6. Human-readable citation URLs are aliases and may be redirected, changed, deprecated, or retargeted to a different record; only the primary `/id/` identifiers are permanent. ## 7. Contribution process diff --git a/src/content/docs/association/statutes.md b/src/content/docs/association/statutes.md index e33a9a2..8a91844 100644 --- a/src/content/docs/association/statutes.md +++ b/src/content/docs/association/statutes.md @@ -107,7 +107,7 @@ In the event of a tied vote on substantive matters, the President decides with t ## Art. 11 Board -The Board consists of at least three persons. +The Board consists of at least two persons. The term of office is two years. Re-election is possible. diff --git a/src/content/docs/community/contributing.md b/src/content/docs/community/contributing.md index 26ca6c2..797f074 100644 --- a/src/content/docs/community/contributing.md +++ b/src/content/docs/community/contributing.md @@ -31,22 +31,21 @@ A contribution does not create a claim to acceptance, prioritization, publicatio ## Review tracks -Changes are routed to one of two tracks: +Changes are routed to one of three tracks: -- **Technical review** — typos, formatting, broken links, minor metadata, `last_checked` updates, uncontested aliases, build / tooling fixes. Needs automated validation and one technical reviewer. -- **Expert review** — new works, new citation systems, new corpora, contested mappings, changes to deterministic ID inputs, status changes (`active` / `deprecated` / `withdrawn` / `blocked`). Needs technical validation, a documented rationale with sources, and at least one expert reviewer. - -The Board reserves decisions on legal or policy-sensitive matters (takedowns, blocking, licence policy). +- **Technical review** — typos, formatting, broken links, minor metadata, `last_checked` updates, uncontested aliases, build / tooling fixes, and merging new registry data as `draft`. Needs automated validation and one technical reviewer. +- **Expert review** — new works, new citation systems, new corpora, contested mappings, changes to deterministic ID inputs, status changes (promotion `draft` → `active`, and `deprecated` / `withdrawn` / `blocked`). Needs technical validation, a documented rationale with sources, and at least one expert reviewer. +- **Board reservation** — takedowns, blocking, licence policy, and other legal or policy-sensitive matters. Decided by the Association Board. ```mermaid flowchart TD S["Contribution
    (issue or PR)"] --> T{Triage} T -->|"typos, formatting, links, metadata, tooling"| TR[Technical review] - T -->|"new work / system / corpus, contested mapping, ID inputs, status change"| ER[Expert review] + T -->|"new work / system / corpus, contested mapping, ID inputs, status change / promotion"| ER[Expert review] T -->|"takedown, blocking, licence / policy"| BR[Board reservation] TR --> V{"Automated validation
    + 1 technical reviewer"} ER --> V2{"Validation + rationale and sources
    + 1 expert reviewer"} - V -->|pass| A([Accepted / merged]) + V -->|pass| A(["Accepted / merged
    (new data lands as draft)"]) V -->|fail| R([Rejected, with reason]) V2 -->|pass| A V2 -->|fail| R @@ -54,6 +53,8 @@ flowchart TD BR -->|decision| R ``` +New registry records enter at `status: draft` after technical review; they stay retractable until an expert review promotes them to `active`, which permanently freezes their identifier (see the [versioning rules](/standard/versioning/) and governance §5). + ## Local development Prerequisites: Node 24 and npm. @@ -70,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. @@ -96,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 @@ -114,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 4a45bc3..a2365a1 100644 --- a/src/content/docs/community/roadmap.md +++ b/src/content/docs/community/roadmap.md @@ -5,11 +5,11 @@ 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 -- **Current spec version:** `v0.1.0-draft` (frozen baseline, unreleased). +- **Current spec version:** `v0.1.0-draft`, published with release `v0.1.0`. All registry data is `draft` (ADR-0004, superseding ADR-0003): identifiers carry no persistence promise until expert review promotes them to `active`. - **Phase:** static registry MVP. The site, governance, standard workspace, schemas, compiler, and seed registry data are in place; public resolver/API behavior is the next milestone. Status legend: done · in progress · planned · blocked. @@ -23,13 +23,13 @@ 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. ## Next -- planned: Publish the Standard draft v0.1.0 as the first citable baseline. +- done: Publish the Standard draft v0.1.0 as the first citable baseline (release `v0.1.0`). - planned: Serve the generated JSON Schema at a stable `/schemas/` path or update the specification if the publication path changes. - planned: Add `Accept-Language` / `edition`-based 303 redirect on `/id/ref/{uuid}` to a matching `resolver_targets` entry. - planned: Publish regular registry exports from the compiled data bundle. diff --git a/src/content/docs/de/association/governance.md b/src/content/docs/de/association/governance.md index 31fded2..fe7e06e 100644 --- a/src/content/docs/de/association/governance.md +++ b/src/content/docs/de/association/governance.md @@ -96,7 +96,8 @@ Ein technischer Review genügt in der Regel für: - kleinere Metadatenkorrekturen; - Aktualisierung von `last_checked`-Feldern; - Ergänzung unstrittiger Aliase; -- Dokumentationskorrekturen ohne fachliche Tragweite. +- Dokumentationskorrekturen ohne fachliche Tragweite; +- Aufnahme neuer Datensätze als `draft` (Merge validierter Daten in das Register). Voraussetzungen: @@ -118,7 +119,8 @@ Ein fachlicher Review ist erforderlich für: - strittige Mapping-Aussagen; - widersprüchliche externe Identifikatoren; - Änderungen mit erheblicher fachlicher oder reputationsbezogener Wirkung; -- Statusänderungen zu `active`, `deprecated`, `withdrawn` oder `blocked`, soweit fachlich begründet. +- Promotion von Datensätzen von `draft` zu `active` (der Zeitpunkt, an dem die Persistenzzusage beginnt); +- Statusänderungen zu `deprecated`, `withdrawn` oder `blocked`, soweit fachlich begründet. Voraussetzungen: @@ -143,16 +145,16 @@ Der Vorstand entscheidet oder bestätigt: ### 5.1 Allgemeine Datensatzstatus -- **candidate:** vorgeschlagener Datensatz, noch nicht vollständig geprüft; -- **active:** gültig und empfohlen; +- **draft:** Datensatz in Arbeit; ohne Tombstone korrigierbar oder zurückziehbar; von der Persistenzzusage ausgenommen; +- **active:** gültig und empfohlen; ab diesem Zeitpunkt dauerhaft identifiziert; - **deprecated:** nicht mehr empfohlen, aber aus historischen Gründen erhalten; - **withdrawn:** zurückgezogen, Landing Page bleibt erhalten; - **blocked:** aus rechtlichen, policybezogenen oder schwerwiegenden Qualitätsgründen gesperrt. ### 5.2 Mapping-Status -- **candidate:** vorgeschlagene Zuordnung; -- **active:** geprüfte und empfohlene Zuordnung; +- **draft:** Zuordnung in Arbeit; ohne Tombstone korrigierbar oder zurückziehbar; +- **active:** geprüfte und empfohlene Zuordnung; ab diesem Zeitpunkt dauerhaft identifiziert; - **deprecated:** nicht mehr empfohlene Zuordnung; - **withdrawn:** zurückgezogene Zuordnung; - **blocked:** gesperrte Zuordnung. @@ -161,7 +163,7 @@ Strittige Mappings behalten den passenden Schema-Status und führen Begründung, ### 5.3 Tombstone-Prinzip -Publizierte IDs werden grundsätzlich nicht hart gelöscht. +Aktive IDs (Status `active` oder ein Tombstone-Status) werden grundsätzlich nicht hart gelöscht. Datensätze im Status `draft` können ohne Landing Page zurückgezogen werden. Bei Rückzug, Sperrung oder Deprecation bleibt eine Landing Page erhalten mit: @@ -176,10 +178,10 @@ Bei Rückzug, Sperrung oder Deprecation bleibt eine Landing Page erhalten mit: 1. Primäre TextRefs-IDs sind unabhängige HTTP-URIs. 2. Externe Identifikatoren wie CTS URNs, Wikidata-IDs, DOIs, ARKs, Perseus-URLs oder Scaife-URLs sind keine primären TextRefs-IDs. -3. Stabile und etablierte Referenzen können deterministische IDs erhalten. -4. Provisorische oder unsichere Objekte erhalten generierte IDs. +3. Alle Registereinträge tragen deterministische IDs, die aus ihren Identitätsfeldern berechenbar sind. +4. Provisorische Einträge werden über den Status `draft` ausgedrückt, der von der Persistenzzusage ausgenommen ist; ihre IDs können bis zur Promotion zu `active` verschwinden oder sich ändern. 5. Einmal publizierte IDs werden nicht geändert, nur weil Labels, Titel, Aliase oder externe Mappings verbessert werden. -6. Lesbare Citation-URLs sind Aliase und können umgeleitet, geändert oder deprecated werden; primäre IDs bleiben massgeblich. +6. Lesbare Citation-URLs sind Aliase und können umgeleitet, geändert, deprecated oder einem anderen Datensatz neu zugeordnet werden; nur die primären `/id/`-Identifikatoren sind dauerhaft. ## 7. Contribution-Prozess diff --git a/src/content/docs/de/association/statutes.md b/src/content/docs/de/association/statutes.md index a7ab3a7..2429399 100644 --- a/src/content/docs/de/association/statutes.md +++ b/src/content/docs/de/association/statutes.md @@ -107,7 +107,7 @@ Bei Stimmengleichheit entscheidet bei Sachgeschäften die Präsidentin oder der ## Art. 11 Vorstand -Der Vorstand besteht aus mindestens drei Personen. +Der Vorstand besteht aus mindestens zwei Personen. Die Amtsdauer beträgt zwei Jahre. Wiederwahl ist möglich. diff --git a/src/content/docs/get-started/authoring.md b/src/content/docs/get-started/authoring.md index 82515ec..f237030 100644 --- a/src/content/docs/get-started/authoring.md +++ b/src/content/docs/get-started/authoring.md @@ -2,10 +2,12 @@ title: Authoring registry data description: How contributors add a work, references, and resolver URLs in compact YAML. sidebar: - order: 6 + order: 7 --- -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 under `dist/dump/`, together with the alias table `aliases.json` and the descriptor `datapackage.json`. 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,53 +19,59 @@ data/ └── systems/{system_key}.yaml # one file per CitationSystem ``` -A `Work` source file declares the work itself, references the citation system it uses, 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; +- 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 -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 work: key: dhammapada preferred_label: Dhammapada - status: candidate + alternative_labels: + - 'Dhp' + - 'Dhammapāda' + status: draft created: 2026-05-31 modified: 2026-05-31 -citation_system: dhammapada-chapter-verse +citation_system: dhammapada-chapter-verse # the work's PREFERRED citation system mappings: - - relation: exactMatch - target_kind: wikidata - identifier: 'https://www.wikidata.org/entity/Q220114' + - relation: alternateOf + identifier: 'https://www.wikidata.org/entity/Q748878' + conforms_to: 'https://www.wikidata.org/' source: manual-curation - status: candidate + status: draft created: 2026-05-31 modified: 2026-05-31 resolvers: - - provider: Project Gutenberg - edition: 'Müller (1881), Gutenberg ebook #2017' + - provider: SuttaCentral + edition: 'Bhikkhu Sujato translation' language: en access: open - url: 'https://www.gutenberg.org/files/2017/2017-h/2017-h.htm#link2HCH{chapter04}' + url: 'https://suttacentral.net/dhp{verseGlobal}/en/sujato' - - provider: Wikisource + - provider: ancient-buddhist-texts.net language: en access: open url_by: chapter: - 1: 'https://en.wikisource.org/wiki/Dhammapada_(Muller)#Chapter_I:_The_Twin-Verses' - 2: 'https://en.wikisource.org/wiki/Dhammapada_(Muller)#Chapter_II:_On_Earnestness' + 1: 'https://ancient-buddhist-texts.net/Texts-and-Translations/Dhammapada/01-Pairs.htm' + 2: 'https://ancient-buddhist-texts.net/Texts-and-Translations/Dhammapada/02-Heedfulness.htm' # … one per chapter you have registered references for - - provider: palikanon.com - language: de - access: open - url: 'https://palikanon.com/khuddaka/dhp/dhp.html#dhp_{verse}' - references: - '1.1' - '1.2' @@ -71,7 +79,9 @@ references: # … ``` -Each reference gets one entry per resolver. The compiler expands `{chapter04}` and `{verse}` from the named capture groups in the citation system's `locator_regex`, 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 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` @@ -93,52 +103,113 @@ 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 go in `alternative_labels`. + +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`. + +### `alternative_labels` + +The other names a scholar searches by. The registry browser matches this list, so a reader who types `NE` or `Nikomachische Ethik` still finds the work. + +```yaml +work: + key: aristotle.nicomachean-ethics + preferred_label: Nicomachean Ethics + alternative_labels: + - 'NE' + - 'EN' + - 'Ethica Nicomachea' + - 'Nikomachische Ethik' +``` + +Rules: + +- Add established forms only: a recognised abbreviation, a Latin or Greek title, or a translated title in a language scholars cite in. Do not invent a short form. +- Keep each entry unique inside the work. Do not repeat the `preferred_label`. The compiler rejects both. +- Two different works may share an entry. `Ethics` fits Aristotle and Spinoza. This is allowed. +- Omit the key when there is nothing to add. Do not author an empty list. +- The field never changes an identifier. It is not part of any UUID seed, so you may add or correct a label at any time. -- Attributed: just the title — `Iliad`, `Republic`, `Tractatus Logico-Philosophicus`. -- Anonymous / collective: the conventional English name — `Tanakh`, `Dhammapada`, `New Testament`. +Do not put these in the list: + +- the author, which goes in `creators`; +- the edition, such as SBLGNT or OCT, which goes on the resolver target; +- an external identifier, such as a Wikidata Q-ID, which goes in `mappings`. ### `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`; encode uncertainty via a `closeMatch` mapping, not in the name string. +- 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 -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. -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. palikanon.com's `#dhp_8`, `#dhp_102`) whose anchors use one running index across all chapters. +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 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), 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 + edition: 'Nestle-Aland, Novum Testamentum Graece, 28th edn (NA28)' + language: grc + access: open + vars: + bookUsfm: + from: book + map: + Matt: MAT + John: JHN + 1Cor: 1CO + 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`. 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. -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. +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. + +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 @@ -150,11 +221,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. 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 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: @@ -172,7 +243,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' @@ -187,28 +258,100 @@ references_range: - kind: book_line counts: [ - 611, 877, 461, 544, 909, 529, 482, 565, 713, 579, 848, 471, 837, 522, - 746, 867, 761, 617, 424, 503, 611, 515, 897, 804, + 611, + 877, + 461, + 544, + 909, + 529, + 482, + 565, + 713, + 579, + 848, + 471, + 837, + 522, + 746, + 867, + 761, + 617, + 424, + 503, + 611, + 515, + 897, + 804, ] # Analects — 517 references from per-book chapter counts: # '1.1', …, '20.5' references_range: - kind: book_chapter - counts: [16, 24, 26, 26, 28, 30, 38, 21, 31, 27, 26, 24, 30, 47, 42, 14, 26, 11, 25, 5] + counts: + [ + 16, + 24, + 26, + 26, + 28, + 30, + 38, + 21, + 31, + 27, + 26, + 24, + 30, + 47, + 42, + 14, + 26, + 11, + 25, + 5, + ] # Genesis — 1,533 references from per-chapter verse counts: -# 'Genesis.1.1', …, 'Genesis.50.26' +# 'Gen.1.1', …, 'Gen.50.26' references_range: - kind: book_chapter_verse - book: Genesis + book: Gen counts: [31, 25, 24, 26, 32, 22 /* …, 26 */] # Dhammapada — 423 references from per-chapter verse counts (no book prefix): # '1.1', …, '26.41' references_range: - kind: chapter_verse - counts: [20, 12, 11, 16, 16, 14, 10, 16, 13, 17, 11, 10, 12, 18, 12, 12, 14, 21, 17, 17, 16, 14, 14, 26, 23, 41] + counts: + [ + 20, + 12, + 11, + 16, + 16, + 14, + 10, + 16, + 13, + 17, + 11, + 10, + 12, + 18, + 12, + 12, + 14, + 21, + 17, + 17, + 16, + 14, + 14, + 26, + 23, + 41, + ] # Bekker — page × {a,b} × lines 1..N, with explicit per-book page ranges: # '1094a1', '1094a2', …, '1181b30' @@ -225,17 +368,51 @@ 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. 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: + key: plato.republic + preferred_label: Republic + status: draft + created: 2026-05-31 + modified: 2026-05-31 + +citation_system: stephanus # preferred system +references: + - '514a' + +additional_systems: + - 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 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. + +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 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 key: dhammapada-chapter-verse preferred_label: Dhammapada chapter-and-verse -normalization_version: 1.0.0 +description: >- + Dhammapada cited by chapter and verse (`chapter.verse`), e.g. 1.1 through + 26.41, following the standard Pali Text Society numbering of 423 verses + in 26 chapters. locator_regex: '^(?[1-9]|1[0-9]|2[0-6])\.(?[1-9][0-9]*)$' # Optional: per-chapter verse counts. When present, the compiler exposes # `{verseGlobal}` (cumulative 1..N) to URL templates of works using this system. @@ -268,34 +445,35 @@ chapter_sizes: 23, 41, ] -examples: - valid: ['1.1', '1.20', '8.3', '26.41'] - invalid: ['0.1', '27.1', '1', '1.0'] -status: candidate +status: draft 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 ```sh -npm run compile:data # expand YAML → JSONL resources plus datapackage.json under dist/dump/ +npm run compile:data # expand YAML → JSONL resources, aliases.json, and datapackage.json under dist/dump/ npm run validate:data # check every record against the canonical Zod schemas 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 (regex, examples, references). 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). -- `/cite/{work_key}/{locator}/` — short alias that redirects to the canonical reference page. +- `/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/dhammapada/1.1` lands on the 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. +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, and publishes it at `/reg/work/{work_key}/aliases.json` and `/dump/aliases.json`. 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/finding-references.md b/src/content/docs/get-started/finding-references.md new file mode 100644 index 0000000..2eb3ab0 --- /dev/null +++ b/src/content/docs/get-started/finding-references.md @@ -0,0 +1,71 @@ +--- +title: Find a reference +description: How the finder at /find/ turns a familiar citation into a canonical TextRefs reference, and what it refuses to guess. +sidebar: + order: 2 +--- + +You know the citation. TextRefs finds the reference. + +The finder at [`/find/`](/find/) takes the string a scholar already writes — "Plato Republic 514a" — and returns the canonical reference for that passage: a permanent link, the editions that carry the passage, and a citation to copy. + +It runs entirely in your browser against the public registry files. There is no search server, and the finder reads nothing that a client cannot read for itself. See [URL layout](/get-started/url-layout/#find-a-reference-by-its-locator) for the same lookup done from code. + +## What you can type + +| You type | The finder reads it as | +| --------------------------- | ---------------------- | +| `Republic` | a work, by its title | +| `Plato` | a work, by its creator | +| `plato.republic` | a work, by its key | +| `Plato Republic 514a` | a work and a passage | +| `Nicomachean Ethics 1094a1` | a work and a passage | + +The last whitespace-separated token becomes the passage when it contains a digit and some registered citation system accepts its shape. That rule keeps `1 Corinthians` intact: `Corinthians` has no digit, so nothing is split off. + +Punctuation and accents are optional. `tractatus logico philosophicus` finds the same work as the hyphenated title. + +## How matches are ranked + +Ranking is a fixed ladder. It is deterministic, it carries no model, and the same query always returns the same order. + +1. Exact work key. +2. Exact preferred title. +3. Exact alternative title, which is where abbreviations and translated titles live. See [`alternative_labels`](/get-started/authoring/#alternative_labels). +4. Creator plus exact title. +5. Title prefix. +6. Token match, across titles, key, and creator names. +7. Fuzzy match, bounded to a small edit distance. + +Only the best tier that matched anything decides the outcome. An exact title is never weighed against a fuzzy near-miss, so a weak match cannot turn a good one into an ambiguity. + +## What the finder will not guess + +This is the part that matters for citation. TextRefs is the authority for identity, so the finder asks rather than picks whenever a query has more than one honest reading. + +**A passage with no work stays a question.** Type `514a` and you get candidates, not a result — even when only one registered work is numbered that way. The query named no text, and today's single candidate becomes several as the registry grows. + +**A shared name stays two hits.** Two works may carry the same alternative label; `Ethics` fits Aristotle and Spinoza. The registry allows this deliberately, and a consumer must treat the match as ambiguous rather than resolve it. + +**One passage number under two systems stays a choice.** A work cited under more than one citation system can repeat a locator string and mean a different passage each time. The finder lists the systems and lets you pick. + +**Fuzzy matching never reaches identity.** It helps find a work and stops there. A reference is identified by an exact lookup of its work, citation system, and locator, so a near-miss cannot invent one. `Republik` finds the _Republic_; `514b` never resolves to `514a`. + +When a passage is rejected, the finder names the numbering the work actually uses and quotes that citation system's own description. It does not work out which rule your locator broke. How much that explains depends on the description: ask for `Republic 327` and the Stephanus entry spells out the locator form, and ask for `Iliad 25.1` and the Homeric entry happens to mention the 24 books that make `25` wrong. + +## What you get back + +A resolved reference shows, in this order: + +- **the work and the passage**, in words — never a UUID as the label; +- **where to read it**: the registered editions, with their language and whether access is open; +- **how to cite it**: a formatted citation, and the permanent link; +- **the registry record** itself, folded away until you ask for it — work key, citation system, status, and the HTML and JSON-LD records. + +The permanent link names the passage, not one edition of it. It keeps working when the editions behind it change. That is the whole point of the reference layer, and [how it works](/get-started/how-it-works/) explains the model underneath. + +A record still marked `draft` is flagged where you would otherwise copy the citation. Draft records may change or be withdrawn, so do not rely on one in published work yet. + +## When a passage is missing + +The registry is filled in work by work and passage by passage. A well-formed passage that is not registered yet says so plainly, and that is a gap in the data rather than a failed lookup. [Browse the registry](/reg/) to see what is in it, or [add a work](/get-started/authoring/). diff --git a/src/content/docs/get-started/how-it-works.md b/src/content/docs/get-started/how-it-works.md index 68d429f..a6f289d 100644 --- a/src/content/docs/get-started/how-it-works.md +++ b/src/content/docs/get-started/how-it-works.md @@ -2,7 +2,7 @@ title: How it works description: A practical walkthrough of TextRefs identity, mappings, and resolver targets. sidebar: - order: 3 + order: 4 --- TextRefs turns traditional scholarly citations into stable, machine-readable identifiers without choosing one edition, translation, website, or provider as authoritative. @@ -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 | | -------------------- | --------------------------------- | --------------------------------------------- | @@ -20,7 +20,7 @@ TextRefs uses three records for identity and one record for work-level equivalen | `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 @@ -29,22 +29,26 @@ For a citation such as "Plato, _Republic_ 514a", a registry contributor or parse 1. Identify the work: `plato.republic`. 2. Identify the citation system: `stephanus`. 3. Normalize the locator according to that citation system: `514a`. -4. Mint the `CanonicalReference` ID from the tuple `work_key`, `citation_system_key`, `locator`, and `normalization_version`. +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 -The identity is the Stephanus reference, not any one Greek text, translation, or platform page. The compiled registry record for [`plato.republic` `514a`](/id/ref/884e8b51-b9cc-5f4b-9e49-60c636c0cd1a/) is: +The identity is the Stephanus reference, not any one Greek text, translation, or platform page. The compiled registry record for [`plato.republic` `514a`](/id/ref/dc799d4b-9b17-5d76-85aa-dfd001c5321d/) is: ```json { - "id": "https://textrefs.org/id/ref/884e8b51-b9cc-5f4b-9e49-60c636c0cd1a", + "id": "https://textrefs.org/id/ref/dc799d4b-9b17-5d76-85aa-dfd001c5321d", "type": "CanonicalReference", "work_key": "plato.republic", "citation_system_key": "stephanus", "locator": "514a", - "normalization_version": "1.0.0", "resolver_targets": [ { "url": "https://www.perseus.tufts.edu/hopper/text?doc=Plat.+Rep.+514a", @@ -52,93 +56,107 @@ 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_url": "https://www.perseus.tufts.edu/hopper/about/copyright", + "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": "candidate", + "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/59a2d83f-6aff-5fbf-b8f7-b243c3ed0594/) contains a resolver target to STEP Bible: +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 { - "id": "https://textrefs.org/id/ref/59a2d83f-6aff-5fbf-b8f7-b243c3ed0594", + "id": "https://textrefs.org/id/ref/b6438d55-f3f2-5fc7-ab40-4f582f8774c3", "type": "CanonicalReference", "work_key": "new-testament", "citation_system_key": "bible-book-chapter-verse", "locator": "John.3.16", - "normalization_version": "1.0.0", "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": "candidate", + "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 { "type": "MappingAssertion", "subject": "https://textrefs.org/id/work/plato.republic", - "relation": "exactMatch", + "relation": "alternateOf", "target": { - "target_kind": "wikidata", - "identifier": "https://www.wikidata.org/entity/Q123397" + "identifier": "https://www.wikidata.org/entity/Q123397", + "conforms_to": "https://www.wikidata.org/" }, "source": "manual-curation", - "status": "candidate", + "status": "draft", "created": "2026-05-31", - "modified": "2026-06-02" + "modified": "2026-08-11" } ``` -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 -For a heavily translated work, many locations can share one reference identity. The current data-backed example is [`new-testament` `John.3.16`](/id/ref/59a2d83f-6aff-5fbf-b8f7-b243c3ed0594/): +For a heavily translated work, many locations can share one reference identity. The current data-backed example is [`new-testament` `John.3.16`](/id/ref/b6438d55-f3f2-5fc7-ab40-4f582f8774c3/): ```json { "type": "CanonicalReference", "work_key": "new-testament", "citation_system_key": "bible-book-chapter-verse", - "locator": "John.3.16", - "normalization_version": "1.0.0" + "locator": "John.3.16" } ``` 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/), 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 and connect them with `closeMatch` mappings. Do not collapse divergent versification, pagination, or segmentation into one identity. +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. +- [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 363cf55..1a5a26a 100644 --- a/src/content/docs/get-started/index.md +++ b/src/content/docs/get-started/index.md @@ -1,42 +1,41 @@ --- title: Get started -description: Why TextRefs exists, who it's for, and how it fits with existing identifier systems. +description: Why a passage needs one persistent identity, and how TextRefs supplies it without replacing existing editions or identifiers. sidebar: order: 1 --- -## The gap +A passage has one identity. The editions that carry it are many. -Citations like "Plato, _Republic_ 514a" or "Aristotle, _Eth. Nic._ 1094a1" are foundational to scholarship in classics, theology, law, philosophy, and adjacent disciplines. Every serious edition, commentary, and database uses them. Yet none of them has a native, persistent, machine-readable identifier you can paste into a tool, link from a paper, or feed to an indexing pipeline. They live as plain text inside footnotes and prose, dependent on the reader knowing the citation convention. +"Plato, _Republic_ 514a" is the same passage in Burnet's Oxford text, the Slings OCT that replaced it, Shorey's Loeb, and every translation that keeps the Stephanus numbers. Pagination, apparatus and language differ. The reference does not. It is the most durable thing in the scholarly record: central to classics, theology, law and philosophy, used by every edition and commentary, and understood across centuries. -That mismatch — central in scholarship, invisible to software — is what TextRefs addresses. +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. -## What TextRefs is +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. -TextRefs is an open registry. For each canonical reference we mint a persistent HTTP URI, attach curated mappings to relevant external identifiers (CTS URNs, Wikidata QIDs, DOIs, library and edition URLs), record resolver targets where readers can inspect the passage, document provenance and uncertainty, and publish everything as JSON-LD under non-profit governance. The registry is read-only and changes happen via reviewed pull requests; data is released under CC0 so it can flow into any tool that needs 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 same model covers a Stephanus passage in Plato, a Bekker line in Aristotle, a chapter-and-verse in the Vulgate, an article in the _Summa_, and a fragment in the _Digesta_ — every traditional reference system is a `CitationSystem` with its own locator rules. +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. -## Use TextRefs with existing systems +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. -Use TextRefs for the stable citation identity: the passage, article, line, section, or fragment a scholar means when they write a traditional reference. Keep edition text, commentary, apparatus, translations, and platform-specific records in the systems that already curate them. - -This division is deliberate. TextRefs stays small, persistent, and legally reusable; libraries, editions, catalogues, and reading platforms keep doing the richer work they are built for. The registry connects them through curated mappings instead of trying to replace them. See the [related systems comparison](/get-started/related-systems/) for the full picture. +[Find a reference](/find/). [Browse the registry](/reg/). [Read the standard](/standard/). [Bring your corpus in](/get-started/authoring/). ## Keep reading +- [Find a reference](/get-started/finding-references/) — how the finder turns a familiar citation into a canonical reference, and what it refuses to guess. - [How it works](/get-started/how-it-works/) — the practical model: identity, mappings, resolver targets, and examples. - [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. ## Live examples -- [`/id/work/dhammapada/`](/id/work/dhammapada/) — Dhammapada with four real providers (Gutenberg, Wikisource, ancient-buddhist-texts.net, palikanon.com) across English and German. +- [`/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. +- [`/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. -- [URL layout](/get-started/url-layout/) — how `/id/`, `/reg/`, `/cite/`, and `/api/` fit together. 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 354936d..1631e75 100644 --- a/src/content/docs/get-started/mappings-and-resolver-targets.md +++ b/src/content/docs/get-started/mappings-and-resolver-targets.md @@ -2,12 +2,12 @@ title: Mappings and resolver targets description: How to model external identifiers, reading URLs, and canonical citation examples in TextRefs. sidebar: - order: 4 + order: 5 --- 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,35 +19,35 @@ 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` 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. -Use `exactMatch` only when the target identifies the same work with enough precision. Use `closeMatch` for edition or scope mismatches. +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 { "type": "MappingAssertion", "subject": "https://textrefs.org/id/work/dhammapada", - "relation": "exactMatch", + "relation": "alternateOf", "target": { - "target_kind": "wikidata", - "identifier": "https://www.wikidata.org/entity/Q220114" + "identifier": "https://www.wikidata.org/entity/Q748878", + "conforms_to": "https://www.wikidata.org/" }, "source": "manual-curation" } ``` -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. @@ -69,37 +69,37 @@ Each entry records what a reader needs to understand the link: `language` (BCP 4 "work_key": "dhammapada", "citation_system_key": "dhammapada-chapter-verse", "locator": "1.1", - "normalization_version": "1.0.0", "resolver_targets": [ { - "url": "https://en.wikisource.org/wiki/Dhammapada_(Muller)#Chapter_I:_The_Twin-Verses", - "language": "en", - "edition": "Müller (1881)", - "provider": "Wikisource", + "url": "https://suttacentral.net/dhp1/pli/ms", + "language": "pli", + "edition": "Mahāsaṅgīti Tipiṭaka", + "provider": "SuttaCentral", "access": "open" }, { - "url": "https://palikanon.com/khuddaka/dhp/dhp.html#dhp_1", - "language": "de", - "provider": "palikanon.com", + "url": "https://ancient-buddhist-texts.net/Texts-and-Translations/Dhammapada/01-Pairs.htm", + "language": "en", + "edition": "Anandajoti Bhikkhu translation", + "provider": "ancient-buddhist-texts.net", "access": "open" } ] } ``` -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 documented normalization rules, valid examples, invalid examples, 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 | | ---------- | ---------------------------------------- | -------------------- | ------------------------------------------------------------- | | Philosophy | Plato, _Republic_ `514a` | Stephanus pagination | Present as `plato.republic` with `stephanus`. | | Philosophy | Aristotle, _Nicomachean Ethics_ `1094a1` | Bekker numbering | Present as `aristotle.nicomachean-ethics` with `bekker`. | | Buddhist | _Dhammapada_ `1.1` | chapter and verse | Present as `dhammapada` with `dhammapada-chapter-verse`. | -| Tanakh | Genesis `Genesis.1.1` | book, chapter, verse | Present as `tanakh` with `bible-book-chapter-verse`. | +| Tanakh | Genesis `Gen.1.1` | book, chapter, verse | Present as `tanakh` with `bible-book-chapter-verse`. | | Bible | John `John.3.16` | book, chapter, verse | Present as `new-testament` with `bible-book-chapter-verse`. | | Chinese | _Analects_ `2.1` | book and chapter | Present as `confucius.analects` with `analects-book-chapter`. | | Chinese | _Daodejing_ `1` | chapter | Present as `laozi.daodejing` with `daodejing-chapter`. | @@ -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 and connect them with `closeMatch` mappings. +**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: -**Contained-by relationships.** If an identifier points to a whole edition, scan, or digital object rather than the exact passage, avoid `exactMatch`. Use `closeMatch` only when the connection is useful and the scope is clear. +- 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 normalized locator; -- the citation system has valid and invalid examples; -- each `MappingAssertion` subject is a Work IRI and its target identifies a textual resource; -- each `exactMatch` is precise enough to survive review; -- 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 7eef875..fd2168f 100644 --- a/src/content/docs/get-started/related-systems.md +++ b/src/content/docs/get-started/related-systems.md @@ -1,41 +1,113 @@ --- -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 + order: 6 --- -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. +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 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 | -| **[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 | -| **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 - **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 and resolver targets from the `mappings` array — 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 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. diff --git a/src/content/docs/get-started/url-layout.md b/src/content/docs/get-started/url-layout.md index 24c1b53..d08cd62 100644 --- a/src/content/docs/get-started/url-layout.md +++ b/src/content/docs/get-started/url-layout.md @@ -2,17 +2,17 @@ title: URL layout description: How /id/, /reg/, /cite/, and /api/ fit together on textrefs.org. sidebar: - order: 7 + order: 8 --- 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 those record pages. Links into `/id/`. | -| `/cite/` | **Cite** | Short, memorable URLs (`/cite/{work}/{locator}`) that redirect to the canonical `/id/` URL. | -| `/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/`. Also serves two JSON-LD collections and one locator index for each work, so a client can find a record without a key or a UUID. | +| `/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: @@ -23,13 +23,14 @@ In one line: Plato's _Republic_ 514a — the Stephanus passage where Socrates begins the Allegory of the Cave — is one canonical reference. Here is what each prefix gives you for it: - **Canonical identifier** — the URL you cite, link from a paper, or paste into a tool: - - `https://textrefs.org/id/ref/884e8b51-b9cc-5f4b-9e49-60c636c0cd1a/` (HTML for browsers) - - `https://textrefs.org/id/ref/884e8b51-b9cc-5f4b-9e49-60c636c0cd1a.json` (JSON-LD for machines) + - `https://textrefs.org/id/ref/dc799d4b-9b17-5d76-85aa-dfd001c5321d/` (HTML for browsers) + - `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: - - `https://textrefs.org/cite/plato.republic/514a` -- **Machine contract** — the OpenAPI describing how `/id/` behaves, so a client knows it can append `.json` to any canonical URL: +- **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 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 @@ -37,20 +38,107 @@ Plato's _Republic_ 514a — the Stephanus passage where Socrates begins the Alle There is no `Accept`-header content negotiation. Every HTML record page advertises its JSON-LD sibling in the document head: ```html - + ``` -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. +## Collections and bulk data + +Every `/id/` URL needs a key you already know. The `/reg/` collections give a client a starting point when it does not have one yet. + +`/reg/` serves two static JSON-LD collections: + +- `https://textrefs.org/reg/works.json` — every work in the registry +- `https://textrefs.org/reg/systems.json` — every citation system in the registry + +Each collection wraps its records in one `@context` and one `@graph` array: + +```json +{ + "@context": "https://textrefs.org/contexts/v1.jsonld", + "@graph": [ + { + "id": "https://textrefs.org/id/work/plato.republic", + "key": "plato.republic", + "type": "Work", + "…": "…" + } + ] +} +``` + +Each item carries no `@context` of its own. The collection lists records of every status — active, draft, and retired — sorted by `key`. + +For bulk use, six artifacts live under `/dump/`: + +- `https://textrefs.org/dump/works.jsonl` +- `https://textrefs.org/dump/citation-systems.jsonl` +- `https://textrefs.org/dump/references.jsonl` +- `https://textrefs.org/dump/mappings.jsonl` +- `https://textrefs.org/dump/aliases.json` — the complete alias table. Each key is a `/cite/` alias or an external identifier. Each value is the canonical `/id/` URL that the key resolves to. +- `https://textrefs.org/dump/datapackage.json` — a Frictionless data-package descriptor. It lists the five resources with a byte count and a `sha256:` hash for each. + +Each `.jsonl` file holds one record per line, with no `@context`. The alias table is one JSON object with sorted keys. Together they are the bulk-archive form of the registry. + +The site is static, and the host sets each `Content-Type` from the file extension. A record or collection `.json` body is JSON-LD by content, but it arrives as `application/json`. The two alias artifacts are plain JSON, and they arrive the same way. A `.jsonl` body arrives as `application/octet-stream`. Parse each body by its documented shape. Do not parse it by the response header. + +Use the small JSON-LD collection for a browser client. Use the `/dump/` files for a bulk consumer. See [`/api/`](/api/) for the full contract. + +## Find a reference by its locator + +This section is the machine path. For the same lookup by hand, use the finder at [`/find/`](/find/), which does exactly these steps in the browser; [Find a reference](/get-started/finding-references/) describes it. + +The UUID of a reference is a UUIDv5 of three fields: the work key, the citation system key, and the locator. A client can compute that UUID, but only with a UUIDv5 implementation. The registry therefore publishes the result as a static file. + +Each work has a locator index at `/reg/work/{work_key}/aliases.json`. The index maps each locator to the UUID of its canonical reference, grouped by citation system key. The body is plain JSON. It is not JSON-LD, and it carries no `@context`. + +To resolve a locator, do these steps: + +1. Send a GET request to `https://textrefs.org/reg/work/plato.republic/aliases.json`. +2. Read the value at `refs["stephanus"]["514a"]`. The value is `dc799d4b-9b17-5d76-85aa-dfd001c5321d`. +3. Send a GET request to `https://textrefs.org/id/ref/dc799d4b-9b17-5d76-85aa-dfd001c5321d.json`. + +Two requests give you the record. No UUID computation is necessary. + +The index has this shape: + +```json +{ + "work_key": "plato.republic", + "preferred_citation_system_key": "stephanus", + "refs": { + "stephanus": { + "327a": "cecef712-e8cf-5878-9d48-419f7d185a56", + "514a": "dc799d4b-9b17-5d76-85aa-dfd001c5321d" + } + } +} +``` + +Obey these rules when you read an index: + +- Build the canonical URL yourself. Add the UUID to `https://textrefs.org/id/ref/`. Add `.json` for the JSON-LD record. +- Use the citation system that you cite. A work with more than one system carries one entry in `refs` for each system. The same locator under two systems denotes a different passage each time. +- Read `preferred_citation_system_key` only for the bare `/cite/{work_key}/{locator}` alias. It is a default for presentation. It does not change identity. +- Do not read a status from the index. The index lists references of every status. Read the canonical record for the status and for the resolver targets. +- Expect an empty `refs` object for a work that has no references yet. + +The largest index is about 1.2 MB, and most are smaller than 70 KB. Use `/dump/aliases.json` if you must resolve locators for many works at one time. + ## 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 always resolve back to the canonical identifier via ``. -- 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 ac4d86f..b3d1100 100644 --- a/src/content/docs/get-started/use-cases.md +++ b/src/content/docs/get-started/use-cases.md @@ -2,43 +2,43 @@ title: Use cases description: Six scenarios where a persistent ID for a canonical reference changes the workflow. sidebar: - order: 2 + order: 3 --- These scenarios are illustrative, not exhaustive. Each pairs "what happens today" with "what TextRefs gives you" so the value is concrete. ## 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, 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 **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. diff --git a/src/content/docs/index.mdx b/src/content/docs/index.mdx index 672196e..44c4a57 100644 --- a/src/content/docs/index.mdx +++ b/src/content/docs/index.mdx @@ -8,8 +8,8 @@ hero: - text: Get started link: /get-started/ icon: right-arrow - - text: Browse registry - link: /reg/ + - text: Find a reference + link: /find/ icon: right-arrow - text: Read the standard link: /standard/ @@ -30,8 +30,9 @@ import RegistrySnapshot from '../../components/RegistrySnapshot.astro'; profiles, data packaging. [Read the spec](/standard/) - Browse the works, citation systems, and canonical references currently - registered. [Open the registry](/reg/) + Type a citation you already know — "Plato Republic 514a" — and get the + canonical reference, the editions that carry the passage, and a citation to + copy. [Find a reference](/find/), or [browse everything registered](/reg/). A static resolver and data API for the registry is planned. Browse the diff --git a/src/content/docs/standard/identifier-syntax.md b/src/content/docs/standard/identifier-syntax.md index f0a9648..d75f46c 100644 --- a/src/content/docs/standard/identifier-syntax.md +++ b/src/content/docs/standard/identifier-syntax.md @@ -8,6 +8,10 @@ sidebar: TextRefs canonical-reference identifiers use deterministic UUID v5 generation. The algorithm is intentionally strict so that independent implementations produce identical identifiers from the same input. +## Purpose of determinism + +Deterministic identity means anyone holding the semantic identity tuple — `work_key`, `citation_system_key`, and the canonical `locator` — can compute the registry UUID offline, without a lookup. The registry remains authoritative for which locators are canonical and attested: a computed UUID identifies a reference, but only a registered `CanonicalReference` makes it a valid TextRefs reference (see [Specification §14](/standard/specification/#14-validation-requirements)). See [ADR-0002](https://github.com/textrefs/textrefs.org/blob/main/decisions/ADR-0002-uuid-seed-semantic-identity.md) for the rationale. + ## Namespace The TextRefs reference namespace UUID is: @@ -16,25 +20,24 @@ The TextRefs reference namespace UUID is: b1a3670e-2ac7-544c-a1b9-396e0dc193f7 ``` -This namespace is derived from `uuidv5(uuid.NAMESPACE_DNS, "textrefs.org/reference")` and is frozen for `v0.1.0-draft`. +This namespace is derived from `uuidv5(uuid.NAMESPACE_DNS, "textrefs.org/reference")` and is frozen for `v0.1.0-draft`. (`NAMESPACE_DNS` here is only a frozen UUID constant used as salt; UUIDv5 treats its name input as opaque bytes, so the slash in the name string is intentional and valid.) ## Seed sequence -The UUID seed string is the following four-field sequence, in this exact order: +The UUID seed string is the following three-field sequence, in this exact order: ```text work_key citation_system_key locator -normalization_version ``` Serialization rules: - Encode the seed as UTF-8. -- Join the four values with a single line feed character, `U+000A`. +- Join the three values with a single line feed character, `U+000A`. - Use each field exactly as normalized, with no leading or trailing whitespace. -- End the seed after `normalization_version`, with no final trailing line feed. +- End the seed after `locator`, with no final trailing line feed. - Use the registry key fields themselves; labels, URIs, aliases, and external identifiers belong in metadata or mappings. - Each field MUST already be normalized by its owning profile before UUID generation. @@ -61,7 +64,8 @@ Deterministic identifiers depend on byte-identical seed strings. Before validati - `locator` MUST NOT contain leading or trailing whitespace, control characters, or internal whitespace unless the citation-system profile explicitly allows it. - Implementations MUST NOT apply NFKC, case folding, digit folding, punctuation folding, transliteration, or script conversion unless the citation-system profile explicitly defines that rule. - Profiles for mixed-script locators MUST state the allowed scripts and enforce them through `locator_regex`. -- Any change to locator normalization that can change a normalized locator MUST change the citation system's `normalization_version`. +- The `locator` stored on a `CanonicalReference` is the canonical spelling defined by the citation-system profile. Non-canonical spellings MUST be rejected at validation time, never silently folded into the canonical form. +- A profile change that would alter the canonical spelling of any accepted locator changes reference identity. It MUST be handled as a registry migration (pre-1.0), a breaking registry release, or — when the distinction is genuinely semantic — a new `citation_system_key`. It MUST NOT be applied silently. The seed bytes used for UUID v5 generation are ASCII-restricted (keys) and NFC-normalized UTF-8 (locators). This is independent of whether downstream TextRefs identifiers are expressed as URIs ([RFC 3986](https://www.rfc-editor.org/rfc/rfc3986)) or IRIs ([RFC 3987](https://www.rfc-editor.org/rfc/rfc3987)). @@ -73,7 +77,6 @@ Input tuple: work_key = plato.republic citation_system_key = stephanus locator = 514a -normalization_version = 1.0.0 ``` Seed string: @@ -82,19 +85,18 @@ Seed string: plato.republic stephanus 514a -1.0.0 ``` Result: ```text -884e8b51-b9cc-5f4b-9e49-60c636c0cd1a +dc799d4b-9b17-5d76-85aa-dfd001c5321d ``` Canonical URI: ```text -https://textrefs.org/id/ref/884e8b51-b9cc-5f4b-9e49-60c636c0cd1a +https://textrefs.org/id/ref/dc799d4b-9b17-5d76-85aa-dfd001c5321d ``` ## MappingAssertion seed @@ -107,7 +109,7 @@ The mapping namespace UUID is: f16bb214-4241-549d-ad41-7b011f02befb ``` -This namespace is derived from `uuidv5(uuid.NAMESPACE_DNS, "textrefs.org/mapping")` and is frozen for `v0.1.0-draft`. +This namespace is derived from `uuidv5(uuid.NAMESPACE_DNS, "textrefs.org/mapping")` and is frozen for `v0.1.0-draft`. (As above, `NAMESPACE_DNS` is a frozen salt constant; the name input is opaque bytes, not a DNS label.) The seed string is the following three-field sequence, joined with single line feed characters and no trailing newline: @@ -117,12 +119,14 @@ relation target.identifier ``` -`subject` MUST be the canonical Work IRI (`https://textrefs.org/id/work/{work_key}`). `relation` MUST be the literal string `exactMatch` or `closeMatch`. `target.identifier` MUST be used as supplied by the source record, after any IRI normalization the source profile already applies. `target.target_kind` is a non-normative hint and does NOT enter the seed. +`subject` MUST be the canonical Work IRI (`https://textrefs.org/id/work/{work_key}`). `relation` MUST be the literal string `alternateOf` or `isReferencedBy`. `target.identifier` MUST be used as supplied by the source record, after any IRI normalization the source profile already applies. `target.conforms_to` is informative and does NOT enter the seed. The canonical URI is `https://textrefs.org/id/mapping/{uuid}`. ## Immutability -Once a deterministic identifier is published, it is permanent. If a record is found to be wrong, it MUST be marked `deprecated`, `withdrawn`, or `blocked`; the original URI MUST remain dereferenceable as a tombstone. +Once a deterministic identifier is published at status `active`, it is permanent. If a promoted record is found to be wrong, it MUST be marked `deprecated`, `withdrawn`, or `blocked`; the original URI MUST remain dereferenceable as a tombstone. + +Records at status `draft` are exempt: they MAY be corrected or retracted without a tombstone, and a retracted draft IRI simply ceases to resolve (see [Specification §11](/standard/specification/#11-identifier-policy) and [Versioning](/standard/versioning/#draft-records-and-retraction)). Because identity is deterministic, a retracted tuple that is later re-proposed regains the same UUID by construction. See [Specification §11](/standard/specification/#11-identifier-policy) for the normative identifier policy. 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 2affd27..a400f10 100644 --- a/src/content/docs/standard/json-ld.md +++ b/src/content/docs/standard/json-ld.md @@ -6,7 +6,7 @@ sidebar: order: 5 --- -TextRefs records are plain JSON that becomes linked data through a published JSON-LD context. The context maps TextRefs terms onto a small TextRefs ontology namespace (`tr:`) plus established vocabularies — SKOS for labels, schemes, and mapping relations, Dublin Core Terms for dates and provenance, schema.org for URLs and providers, and XSD for date typing. +TextRefs records are plain JSON that becomes linked data through a published JSON-LD context. The context maps TextRefs terms onto a small TextRefs ontology namespace (`tr:`) plus established vocabularies — SKOS for labels and schemes, PROV-O and Dublin Core Terms for mapping relations, Dublin Core Terms also for dates and provenance, schema.org for URLs and providers, and XSD for date typing. The `v1` context is served at: @@ -16,22 +16,25 @@ 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, schemes (`inScheme`), and mapping relations | -| `dcterms` | `http://purl.org/dc/terms/` | `created`, `modified`, `source`, `language`, `license` | -| `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 -The MVP mapping relations map directly onto SKOS: +The mapping relations are chosen by what the target is, never by author confidence: -- `exactMatch` → `skos:exactMatch` -- `closeMatch` → `skos:closeMatch` +- `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). -Use `exactMatch` only when the mapped object identifies the same reference with sufficient precision. If there is uncertainty about segmentation, edition, translation, coverage, or locator alignment, use `closeMatch`. See [Specification §10](/standard/specification/#10-mappingassertion). +Published `Work` records additionally carry direct `alternateOf` / `isReferencedBy` arrays derived from mapping assertions that are not `deprecated`, `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)). The arrays enrich the work. They make no claim about review: a consumer that needs the status of a mapping must read the `MappingAssertion`. + +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). ## The context document @@ -41,6 +44,7 @@ Use `exactMatch` only when the mapped object identifies the same reference with "tr": "https://textrefs.org/ontology#", "skos": "http://www.w3.org/2004/02/skos/core#", "dcterms": "http://purl.org/dc/terms/", + "prov": "http://www.w3.org/ns/prov#", "schema": "https://schema.org/", "xsd": "http://www.w3.org/2001/XMLSchema#", "id": "@id", @@ -51,6 +55,8 @@ Use `exactMatch` only when the mapped object identifies the same reference with "MappingAssertion": "tr:MappingAssertion", "key": "tr:key", "preferred_label": "skos:prefLabel", + "alternative_labels": { "@id": "skos:altLabel", "@container": "@set" }, + "description": "dcterms:description", "creators": "schema:creator", "kind": "tr:creatorKind", "family": "schema:familyName", @@ -59,34 +65,32 @@ Use `exactMatch` only when the mapped object identifies the same reference with "inScheme": { "@id": "skos:inScheme", "@type": "@id" }, "work_key": "tr:workKey", "citation_system_key": "tr:citationSystemKey", + "preferred_citation_system_key": "tr:preferredCitationSystemKey", "locator": "tr:locator", - "normalization_version": "tr:normalizationVersion", "status": "tr:status", "source": "dcterms:source", "created": { "@id": "dcterms:created", "@type": "xsd:date" }, "modified": { "@id": "dcterms:modified", "@type": "xsd:date" }, "relation": "tr:relation", - "exactMatch": { "@id": "skos:exactMatch", "@type": "@id" }, - "closeMatch": { "@id": "skos:closeMatch", "@type": "@id" }, + "alternateOf": { "@id": "prov:alternateOf", "@type": "@id" }, + "isReferencedBy": { "@id": "dcterms:isReferencedBy", "@type": "@id" }, "subject": { "@id": "tr:subject", "@type": "@id" }, "target": "tr:target", - "target_kind": "tr:targetKind", "identifier": { "@id": "tr:identifier", "@type": "@id" }, + "conforms_to": { "@id": "dcterms:conformsTo", "@type": "@id" }, "resolver_targets": "tr:resolverTargets", "provider": "schema:provider", "url": { "@id": "schema:url", "@type": "@id" }, "language": "dcterms:language", "edition": "schema:bookEdition", "access": "tr:access", - "license": "dcterms:license", + "license": { "@id": "dcterms:license", "@type": "@id" }, "license_url": { "@id": "dcterms:license", "@type": "@id" }, + "superseded_by": { "@id": "dcterms:isReplacedBy", "@type": "@id" }, "last_checked": { "@id": "tr:lastChecked", "@type": "xsd:date" }, - "locator_regex": "tr:locatorRegex", - "examples": "tr:examples", - "valid": "tr:validExample", - "invalid": "tr:invalidExample" + "locator_regex": "tr:locatorRegex" } } ``` -`key`, `work_key`, and `citation_system_key` are plain strings in the core JSON format. Rich bibliographic and authority data — catalogue records, edition histories, subject classifications — belongs in external systems and is connected to TextRefs records through `MappingAssertion`s. The one in-record exception is the optional `Work.creators` array, which carries minimal authorship for citation rendering (see [Specification §6](/standard/specification/#6-work)). The `license` term carries an SPDX identifier string; `license_url` (optional fallback) carries an IRI. `MappingAssertion.source` is a plain string in v0.1 — a structured **W3C PROV-O** mapping (`prov:wasDerivedFrom`) is reserved for a later context version. +`key`, `work_key`, `citation_system_key`, and `preferred_citation_system_key` are plain strings in the core JSON format. Rich bibliographic and authority data — catalogue records, edition histories, subject classifications — belongs in external systems and is connected to TextRefs records through `MappingAssertion`s. The one in-record exception is the optional `Work.creators` array, which carries minimal authorship for citation rendering (see [Specification §6](/standard/specification/#6-work)). The `license` term carries the canonical SPDX licence IRI (`https://spdx.org/licenses/{id}`, derived from the authored SPDX identifier at compile time); `license_url` (optional fallback for non-SPDX terms) carries an IRI. Both map to an IRI-typed `dcterms:license`. `MappingAssertion.source` is a plain string in v0.1 — a structured **W3C PROV-O** mapping (`prov:wasDerivedFrom`) is reserved for a later context version. diff --git a/src/content/docs/standard/specification.md b/src/content/docs/standard/specification.md index bf6d03f..38440fa 100644 --- a/src/content/docs/standard/specification.md +++ b/src/content/docs/standard/specification.md @@ -14,7 +14,7 @@ sidebar: TextRefs defines a minimal registry standard for stable, machine-addressable references to texts. -A conforming TextRefs registry MUST provide persistent identifiers for canonical references and MUST describe the citation systems by which those references are formed. It MAY record dereferenceable locations for those references and curated mappings to external identifiers or other references. +A conforming TextRefs registry MUST provide persistent identifiers for canonical references and MUST describe the citation systems by which those references are formed. It SHOULD record dereferenceable locations for those references and MAY record curated mappings to external identifiers or other references. The standard is deliberately small. Its centre is a single idea: **a reference is an abstract identity, separate from any location, edition, or translation where the referenced text can be read.** @@ -27,8 +27,8 @@ A dataset conforms to the TextRefs Standard if it satisfies all of the following 3. Every `Work.key` and `CitationSystem.key` is a flat, stable key that occupies one URI path segment. 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 valid and invalid examples for automated tests. -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`. +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 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,21 +68,22 @@ classDiagram +URI id +string key +string preferred_label + +string[] alternative_labels + +string preferred_citation_system_key +Creator[] creators } class CitationSystem { +URI id +string key +string preferred_label + +string description +string locator_regex - +string normalization_version } class CanonicalReference { +URI id +string work_key +string citation_system_key +string locator - +string normalization_version +ResolverTargetEntry[] resolver_targets } class ResolverTargetEntry { @@ -121,18 +122,27 @@ A `Work.key` is a single flat registry key used to identify the abstract work in "key": "plato.republic", "type": "Work", "preferred_label": "Republic", + "alternative_labels": ["Politeia", "Der Staat"], + "preferred_citation_system_key": "stephanus", "creators": [{ "kind": "person", "family": "Plato" }], - "status": "candidate", + "status": "active", "created": "2026-05-31", "modified": "2026-05-31" } ``` -Required: `id`, `key`, `type` (`Work`), `preferred_label`, `status`, plus administrative metadata ([§12](#12-administrative-metadata)). Optional: `creators`. The `id` MUST be a persistent TextRefs HTTP URI of the form `https://textrefs.org/id/work/{key}`, where `{key}` is one flat key and occupies exactly one URI path segment. The `key` MUST be stable and suitable for deterministic identity generation. +Required: `id`, `key`, `type` (`Work`), `preferred_label`, `preferred_citation_system_key`, `status`, plus administrative metadata ([§12](#12-administrative-metadata)). Optional: `alternative_labels`, `creators`, `alternateOf`, `isReferencedBy`. The `id` MUST be a persistent TextRefs HTTP URI of the form `https://textrefs.org/id/work/{key}`, where `{key}` is one flat key and occupies exactly one URI path segment. The `key` MUST be stable and suitable for deterministic identity generation. + +- `preferred_citation_system_key` MUST reference a known `CitationSystem`. It governs the bare `/cite/{work_key}/{locator}/` alias and default presentation only; it is identity-neutral and MUST NOT affect the validation or resolution of a fully qualified reference ([§7](#7-citationsystem)). +- A `Work` MAY be referenced under more than one `CitationSystem`, of which exactly one is preferred. `creators`, when present, is an array of entries discriminated by `kind`. A `person` entry has `family` (required) and `given` (optional); mononyms such as Plato or Homer use `family` alone, following CSL convention. A `literal` entry has `name` and is the escape hatch for pseudonymous, collective, or institutional authorship (e.g. `[Pseudo-]Aristotle`, an editorial committee). Anonymous works and canonical corpora such as the Bible simply omit `creators`. For attributed-but-disputed works, record the traditional attribution in `creators` and express uncertainty through mappings or editorial review notes, not in the name string. Implementations MUST treat the field as optional and MUST NOT infer authorship from `preferred_label` or `key`. -External identifiers for a `Work` (e.g. Wikidata Q-ID, DOI, VIAF) are recorded as `MappingAssertion`s whose `subject` is the `Work`. They are not fields on the `Work` itself. +`alternative_labels`, when present, is an array of additional names for the work: abbreviations such as `NE`, translated titles such as `Nikomachische Ethik`, and other established short forms. It is published as `skos:altLabel`. The field serves search and display only. It is identity-neutral: no label is an input to any deterministic UUID seed ([§11](#11-identifier-policy)), so adding, editing, or removing an alternative label MUST NOT move an identifier. Each entry MUST be unique within the work and MUST NOT repeat the `preferred_label`. A work with no additional names MUST omit the field rather than publish an empty array. Two different works MAY claim the same alternative label — `Ethics` fits both Aristotle and Spinoza — and implementations MUST treat such a match as ambiguous rather than as an error. Implementations MUST NOT derive identity, authorship, or a citation system from an alternative label. + +External identifiers for a `Work` (e.g. Wikidata Q-ID, DOI, VIAF) are asserted as `MappingAssertion`s whose `subject` is the `Work` ([§10](#10-mappingassertion)). They MUST NOT be authored directly on the `Work`. + +`alternateOf` and `isReferencedBy` are the compiler's read-only projection of those assertions — every one whose `status` is not `deprecated`, `withdrawn` or `blocked`, grouped by `relation` — published straight from the work IRI as `prov:alternateOf` / `dcterms:isReferencedBy` edges ([JSON-LD](/standard/json-ld/#mapping-relations)). They carry no status or provenance; the `MappingAssertion` stays authoritative. The arrays enrich the work. They make no claim about review: a consumer that needs the status of a mapping MUST read the `MappingAssertion`. ## 7. CitationSystem @@ -145,34 +155,30 @@ A `CitationSystem.key` is a single flat registry key for a locator notation and "id": "https://textrefs.org/id/system/bible-book-chapter-verse", "key": "bible-book-chapter-verse", "type": "CitationSystem", - "preferred_label": "Bible book-chapter-verse (OSIS-style)", - "normalization_version": "1.0.0", - "locator_regex": "^(?[A-Za-z][A-Za-z0-9_]*)\\.(?[1-9][0-9]*)\\.(?[1-9][0-9]*)$", - "examples": { - "valid": ["Genesis.1.1", "Psalms.23.1", "Matthew.5.3"], - "invalid": ["Genesis.0.1", "Genesis.1", "1.1.1", "Genesis 1:1"] - }, - "status": "candidate", + "preferred_label": "Bible book-chapter-verse (OSIS)", + "description": "OSIS locator: `Book.Chapter.Verse`. The canonical book vocabulary is the OSIS book abbreviation list (CrossWire), case-sensitive as published there — e.g. `Gen`, `Exod`, `Matt`, `John`, `1Cor`. Chapter and verse are positive integers without leading zeros. Both the Hebrew Bible and the New Testament cite with this grammar.", + "locator_regex": "^(?[1-4]?[A-Za-z][A-Za-z0-9]*)\\.(?[1-9][0-9]*)\\.(?[1-9][0-9]*)$", + "status": "active", "created": "2026-05-31", "modified": "2026-05-31" } ``` -Required: `id`, `key`, `type` (`CitationSystem`), `preferred_label`, `normalization_version`, `locator_regex`, `examples.valid`, `examples.invalid`, plus administrative metadata. The `id` MUST be a persistent TextRefs HTTP URI of the form `https://textrefs.org/id/system/{key}`, where `{key}` is one flat key and occupies exactly one URI path segment. +Required: `id`, `key`, `type` (`CitationSystem`), `preferred_label`, `description`, `locator_regex`, plus administrative metadata. The `id` MUST be a persistent TextRefs HTTP URI of the form `https://textrefs.org/id/system/{key}`, where `{key}` is one flat key and occupies exactly one URI path segment. +- `description` documents the citation tradition and its canonical locator form in prose, including any canonical-form rules that cannot be expressed in `locator_regex`. - `locator_regex` MUST be a valid ECMAScript regular expression. - `locator_regex` provides machine-checkable pre-validation for locator shape only; it need not fully describe citation systems whose valid references cannot be expressed completely as a regular language. - Citation systems SHOULD use an anchored `locator_regex` when the pattern is intended to describe the full locator string. - Regex success does not by itself prove that a reference point exists in a work. -- `normalization_version` MUST use semantic versioning. -- `examples.valid` MUST all match `locator_regex`; `examples.invalid` MUST all fail it. - Unicode handling for keys and locators MUST follow [Identifier syntax](/standard/identifier-syntax/#unicode-normalization). -- A pull request that adds or changes a citation system MUST include the profile, valid examples, and invalid examples. See [Citation-system profiles](/standard/system-profiles/). +- A pull request that adds or changes a citation system MUST include the full profile record. See [Citation-system profiles](/standard/system-profiles/). +- A `Work` MAY be cited under more than one `CitationSystem`, and the same locator string MAY denote different passages under different systems — which is why `citation_system_key` is part of `CanonicalReference` identity ([§11](#11-identifier-policy)). - A `CanonicalReference` links to its citation system through `citation_system_key`. JSON-LD serializations MAY additionally expose that relation with `skos:inScheme`. ## 8. CanonicalReference -A `CanonicalReference` represents one atomized, **language-independent** reference point, identified by combining a work, a citation system, a normalized locator, and a normalization version. It also carries the set of dereferenceable external locations for that reference as an embedded `resolver_targets` array (see [§9](#9-embedded-resolver-targets)). +A `CanonicalReference` represents one atomized, **language-independent** reference point, identified by combining a work, a citation system, and a canonical locator. It also carries the set of dereferenceable external locations for that reference as an embedded `resolver_targets` array (see [§9](#9-embedded-resolver-targets)). ```json { @@ -181,7 +187,6 @@ A `CanonicalReference` represents one atomized, **language-independent** referen "work_key": "new-testament", "citation_system_key": "bible-book-chapter-verse", "locator": "John.3.16", - "normalization_version": "1.0.0", "resolver_targets": [ { "url": "https://www.stepbible.org/?q=version=SBLG|reference=John.3.16", @@ -189,22 +194,22 @@ A `CanonicalReference` represents one atomized, **language-independent** referen "edition": "SBL Greek New Testament", "provider": "STEP Bible", "access": "open", - "license": "CC-BY-4.0" + "license": "https://spdx.org/licenses/CC-BY-4.0" } ], - "status": "candidate", + "status": "active", "created": "2026-05-31", "modified": "2026-05-31" } ``` -Required: `id`, `type` (`CanonicalReference`), `work_key`, `citation_system_key`, `locator`, `normalization_version`, `resolver_targets` (MAY be empty), plus administrative metadata. +Required: `id`, `type` (`CanonicalReference`), `work_key`, `citation_system_key`, `locator`, `resolver_targets` (MAY be empty), plus administrative metadata. - `work_key` MUST reference a known `Work`; `citation_system_key` MUST reference a known `CitationSystem`. - `work_key` and `citation_system_key` MUST be treated as opaque flat keys. Implementations MUST NOT infer author, corpus, title, hierarchy, or resolver behaviour by splitting either key. -- `locator` MUST match the system's `locator_regex`; additional profile-specific validation MAY be required for systems that are not fully regex-checkable. +- `locator` MUST be the canonical spelling defined by the citation-system profile and MUST match the system's `locator_regex`; additional profile-specific validation MAY be required for systems that are not fully regex-checkable. Non-canonical spellings MUST be rejected at validation time, never silently normalized (see [Identifier syntax](/standard/identifier-syntax/#unicode-normalization)). - An accepted `CanonicalReference` MUST represent an attested reference point for the referenced `Work` under the referenced `CitationSystem`. -- `normalization_version` is part of the reference's identity and is fixed when the reference is minted; it records the normalization in force at that time and need not equal the citation system's current `normalization_version`. Its correctness is verified by the deterministic identifier (see [§14](#14-validation-requirements) and [Identifier syntax](/standard/identifier-syntax/)). +- A profile change that would alter the canonical spelling of any accepted locator changes reference identity and MUST be handled as a registry migration, a breaking registry release, or a new `citation_system_key` (see [Identifier syntax](/standard/identifier-syntax/#unicode-normalization)). - The `id` MUST be generated deterministically per [Identifier syntax](/standard/identifier-syntax/); its UUID component is the deterministic seed output. - `resolver_targets` MUST validate per [§9](#9-embedded-resolver-targets). @@ -219,7 +224,7 @@ Required: `id`, `type` (`CanonicalReference`), `work_key`, `citation_system_key` "edition": "King James Version", "provider": "Bible Gateway", "access": "open", - "license": "CC0-1.0", + "license": "https://spdx.org/licenses/CC0-1.0", "license_url": null, "last_checked": "2026-01-01" } @@ -230,27 +235,27 @@ 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 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. 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. ## 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 { "id": "https://textrefs.org/id/mapping/{uuid}", "type": "MappingAssertion", "subject": "https://textrefs.org/id/work/new-testament", - "relation": "exactMatch", + "relation": "alternateOf", "target": { - "target_kind": "wikidata", - "identifier": "https://www.wikidata.org/entity/Q18813" + "identifier": "https://www.wikidata.org/entity/Q18813", + "conforms_to": "https://www.wikidata.org/" }, "source": "manual-curation", - "status": "candidate", + "status": "active", "created": "2026-05-31", "modified": "2026-05-31" } @@ -260,8 +265,8 @@ Required: `id`, `type` (`MappingAssertion`), `subject`, `relation`, `target`, `s - `subject` MUST be a `Work` IRI of the form `https://textrefs.org/id/work/{work_key}`. Per-passage external identifiers (e.g. the CTS URN of a single verse) are derived from work-level mappings combined with the reference locator at resolve time; they MUST NOT be stored as separate `MappingAssertion` records. - `target.identifier` MUST be an IRI ([RFC 3987](https://www.rfc-editor.org/rfc/rfc3987)) that identifies a **textual resource**: a work, edition, manuscript, citation system, or another TextRefs `Work`. -- `target.target_kind` is OPTIONAL and is a human-readable scheme hint (e.g. `"cts"`, `"doi"`, `"wikidata"`, `"textrefs"`). Validators MUST NOT key behaviour off it. The presence or absence of `target_kind` carries no normative weight; the IRI in `identifier` is authoritative. See [Appendix B](#appendix-b-well-known-external-identifier-schemes-informative) for non-normative examples. -- `relation` MUST be one of the SKOS-compatible values `exactMatch` or `closeMatch`. Use `exactMatch` only when the mapped resource identifies the same work with sufficient precision; if there is any uncertainty about edition, coverage, or work boundaries, use `closeMatch`. +- `target.conforms_to` is OPTIONAL. When present, it MUST be a dereferenceable IRI — or an array of such IRIs — identifying the specification or identifier scheme to which `target.identifier` conforms (e.g. the home page of the CTS specification, the Wikidata project, or a DOI handbook section). It is informative: validators MUST NOT key behaviour off it; the IRI in `identifier` is authoritative. See [Appendix B](#appendix-b-well-known-external-identifier-schemes-informative) for non-normative examples. +- `relation` MUST be chosen by what `target.identifier` denotes, never by author confidence: 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). - `source` documents the basis for the assertion. A structured [W3C PROV-O](https://www.w3.org/TR/prov-o/) mapping is reserved for a future version. ## 11. Identifier policy @@ -270,13 +275,15 @@ TextRefs identifiers MUST be persistent HTTP URIs ([RFC 3986](https://www.rfc-ed `Work` identifiers MUST use `https://textrefs.org/id/work/{key}` and `CitationSystem` identifiers MUST use `https://textrefs.org/id/system/{key}`. In both cases `{key}` is the complete flat key and MUST NOT contain additional path segments. For example, `https://textrefs.org/id/work/plato.republic` is valid; `https://textrefs.org/id/work/plato/republic` is not. -A `CanonicalReference` identifier MUST be generated deterministically. The identity seed MUST include `work_key`, `citation_system_key`, `locator`, and `normalization_version`, in that order (see [Identifier syntax](/standard/identifier-syntax/)). +A `CanonicalReference` identifier MUST be generated deterministically. The identity seed MUST include `work_key`, `citation_system_key`, and `locator`, in that order (see [Identifier syntax](/standard/identifier-syntax/)). A `MappingAssertion` identifier MUST be generated deterministically from `subject`, `relation`, and `target.identifier`, in that order, using the `mapping` namespace (see [Identifier syntax](/standard/identifier-syntax/#mappingassertion-seed)). It MUST remain UUID-based and MUST NOT be derived from provider URLs, corpus paths, or resolver structures. Resolver-target entries do not have their own identifiers. -An implementation MUST NOT silently change the identity-defining fields of an existing `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` or `withdrawn`, [§12](#12-administrative-metadata)) and SHOULD be linked to its replacement through an `exactMatch` `MappingAssertion` ([§10](#10-mappingassertion)). +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 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. +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. ## 12. Administrative metadata @@ -292,54 +299,84 @@ Every registry object MUST include: - `created` and `modified` MUST be [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) calendar dates in `YYYY-MM-DD` form. - `status` MUST be one of: - - `candidate` — proposed but not yet accepted as stable. - - `active` — accepted and recommended for use. + - `draft` — work-in-progress record under review; **excluded from the identifier persistence policy** ([§11](#11-identifier-policy)). May be corrected or retracted without a tombstone. + - `active` — accepted, recommended for use, and permanently identified. Promotion from `draft` attaches the persistence promise. - `deprecated` — retained but no longer recommended. - - `withdrawn` — removed from active use because it was erroneous or has been superseded. If a successor exists, it is linked by an `exactMatch` `MappingAssertion`; see [Versioning](/standard/versioning/) for tombstones. + - `withdrawn` — removed from active use because it was erroneous or has been superseded. - `blocked` — retained as a visible tombstone because of a rights, trust, or policy dispute. +An incorrect `active` record is never deleted and its identity-defining fields are never mutated: it MUST instead be moved to `deprecated`, `withdrawn`, or `blocked`, with `superseded_by` set to the successor's IRI when a successor exists. Because identity is deterministic, the corrected tuple mints a new UUID at a new IRI; the original IRI keeps resolving as a tombstone. + Deprecated, withdrawn, and blocked records SHOULD remain visible unless removal is required for legal, privacy, or safety reasons. +`superseded_by` is OPTIONAL: the IRI of the record that replaces this one, published as `dcterms:isReplacedBy`. It MUST NOT appear unless `status` is `deprecated`, `withdrawn`, or `blocked` — a record still in use has no successor. Consumers follow it to reach the successor ([Versioning](/standard/versioning/#tombstones-and-re-minted-records)). + ## 13. Worked example: a multi-translation work -This is the case that motivates separating identity from location. The New Testament exists in many editions and translations, yet `John.3.16` is **one** reference in the OSIS-style book-chapter-verse system. +This is the case that motivates separating identity from location. The New Testament exists in many editions and translations, yet `John.3.16` is **one** reference in the OSIS book-chapter-verse system. **One identity** — a single `Work`, `CitationSystem`, and `CanonicalReference`. The reference embeds all language-tagged locations as `resolver_targets`: ```json { - "work": { - "key": "new-testament", - "type": "Work", - "preferred_label": "New Testament" - }, - "citation_system": { - "key": "bible-book-chapter-verse", - "type": "CitationSystem", - "locator_regex": "^(?[A-Za-z][A-Za-z0-9_]*)\\.(?[1-9][0-9]*)\\.(?[1-9][0-9]*)$" - }, - "canonical_reference": { - "type": "CanonicalReference", - "work_key": "new-testament", - "citation_system_key": "bible-book-chapter-verse", - "locator": "John.3.16", - "resolver_targets": [ - { - "url": "https://www.stepbible.org/?q=version=SBLG|reference=John.3.16", - "language": "grc", - "edition": "SBL Greek New Testament", - "provider": "STEP Bible", - "access": "open", - "license": "CC-BY-4.0" - } - ] - } + "@context": "https://textrefs.org/contexts/v1.jsonld", + "@graph": [ + { + "id": "https://textrefs.org/id/work/new-testament", + "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" + }, + { + "id": "https://textrefs.org/id/system/bible-book-chapter-verse", + "key": "bible-book-chapter-verse", + "type": "CitationSystem", + "preferred_label": "Bible book-chapter-verse (OSIS)", + "description": "OSIS locator: `Book.Chapter.Verse`. The canonical book vocabulary is the OSIS book abbreviation list (CrossWire), case-sensitive as published there — e.g. `Gen`, `Exod`, `Matt`, `John`, `1Cor`. Chapter and verse are positive integers without leading zeros. Both the Hebrew Bible and the New Testament cite with this grammar.", + "locator_regex": "^(?[1-4]?[A-Za-z][A-Za-z0-9]*)\\.(?[1-9][0-9]*)\\.(?[1-9][0-9]*)$", + "status": "active", + "created": "2026-05-31", + "modified": "2026-05-31" + }, + { + "id": "https://textrefs.org/id/ref/b6438d55-f3f2-5fc7-ab40-4f582f8774c3", + "type": "CanonicalReference", + "work_key": "new-testament", + "citation_system_key": "bible-book-chapter-verse", + "locator": "John.3.16", + "resolver_targets": [ + { + "url": "https://www.stepbible.org/?q=version=SBLG|reference=John.3.16", + "language": "grc-Grek", + "edition": "SBL Greek New Testament", + "provider": "STEP Bible", + "access": "open", + "license": "https://spdx.org/licenses/CC-BY-4.0" + }, + { + "url": "https://www.biblegateway.com/passage/?search=John%203%3A16&version=KJV", + "language": "en", + "edition": "King James Version", + "provider": "Bible Gateway", + "access": "open", + "license": "https://spdx.org/licenses/CC0-1.0" + } + ], + "status": "active", + "created": "2026-05-31", + "modified": "2026-05-31" + } + ] } ``` Adding another edition or translation appends one entry to `resolver_targets`. The reference identity — its UUID, its work, its citation system, its locator — does not change. -**Divergent versification** is the one case that _does_ create separate references. Where traditions number verses differently (e.g. the Psalms in the Masoretic text versus the Vulgate/Septuagint), each tradition is a distinct `CitationSystem`, its references are distinct `CanonicalReference`s, and the equivalence between them is recorded as a `closeMatch` `MappingAssertion` — not by collapsing them into one identity. +**Divergent versification** is the one case that _does_ create separate references. Where traditions number verses differently (e.g. the Psalms in the Masoretic text versus the Vulgate/Septuagint), each tradition is a distinct `CitationSystem`, its references are distinct `CanonicalReference`s — not collapsed into one identity. The equivalence between citation systems is not yet expressible in this version: `MappingAssertion.subject` MUST be a `Work` IRI ([§10](#10-mappingassertion)), so a system-to-system assertion cannot be authored. A future revision may widen `subject` to admit a `CitationSystem` IRI. ## 14. Validation requirements @@ -349,14 +386,15 @@ A conforming validator MUST check: 2. object `type` values and TextRefs URI patterns, including `Work` and `CitationSystem` IDs whose keys occupy exactly one path segment; 3. flat-key syntax and uniqueness for `Work.key` and `CitationSystem.key`; 4. administrative metadata and `status` values; -5. citation-system `locator_regex` syntax, and its valid/invalid examples; -6. canonical-reference locator syntax (the `normalization_version` is the value fixed at minting, verified by the deterministic identifier in item 8, not matched against the system's current version); +5. citation-system `locator_regex` syntax; +6. canonical-reference locator syntax: the locator MUST be the profile's canonical spelling and match `locator_regex`; non-canonical spellings MUST be rejected, not normalized; 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. +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`. A validator SHOULD report errors in a machine-readable format, and SHOULD distinguish syntactically valid, registered, mapped, and resolvable references. An input locator that matches `locator_regex` but has no corresponding registered `CanonicalReference` is syntactically valid but not a valid TextRefs reference. @@ -378,12 +416,13 @@ This standard relies on the following external standards. Each is normative wher | Dates | [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) | | URIs | [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986) | | IRIs | [RFC 3987](https://www.rfc-editor.org/rfc/rfc3987) | -| UUIDs | [RFC 4122](https://www.rfc-editor.org/rfc/rfc4122) | +| UUIDs | [RFC 9562](https://www.rfc-editor.org/rfc/rfc9562) | | Unicode normalization (NFC) | [Unicode Standard Annex #15](https://www.unicode.org/reports/tr15/) | | 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/) | @@ -401,19 +440,19 @@ Build on the core registry by keeping these concerns in application, extension, ## Appendix B. Well-known external identifier schemes (informative) -The following identifier schemes commonly satisfy [§10](#10-mappingassertion)'s "textual resource" rule and are useful values for `MappingAssertion.target.identifier`. Treat this table as implementation guidance: the authoritative rule is still whether the IRI identifies a textual resource. - -| Scheme | `target_kind` hint | Example identifier | -| -------- | ------------------ | ------------------------------------------------- | -| TextRefs | `textrefs` | `https://textrefs.org/id/ref/988e0b39-…` | -| CTS URN | `cts` | `urn:cts:greekLit:tlg0031.tlg004:3.16` | -| DTS | `dts` | `https://dts.example/api/collection?id=urn:cts:…` | -| DOI | `doi` | `https://doi.org/10.5281/zenodo.7702622` | -| ARK | `ark` | `https://n2t.net/ark:/12148/btv1b8451636f` | -| Handle | `handle` | `https://hdl.handle.net/1887/4531` | -| PURL | `purl` | `https://purl.org/dc/terms/` | -| URN:NBN | `urn-nbn` | `urn:nbn:de:bvb:12-bsb00012345-2` | -| Wikidata | `wikidata` | `https://www.wikidata.org/entity/Q42` | +The following identifier schemes commonly satisfy [§10](#10-mappingassertion)'s "textual resource" rule and are useful values for `MappingAssertion.target.identifier`. Treat this table as implementation guidance: the authoritative rule is still whether the IRI identifies a textual resource. The `conforms_to` column gives a representative scheme IRI; any dereferenceable IRI that identifies the same specification is equally valid. + +| Scheme | Example identifier | Example `conforms_to` | +| -------- | ------------------------------------------------- | ------------------------------------------------------------- | +| TextRefs | `https://textrefs.org/id/ref/988e0b39-…` | `https://textrefs.org/` | +| CTS URN | `urn:cts:greekLit:tlg0031.tlg004:3.16` | `https://cite-architecture.github.io/` | +| DTS | `https://dts.example/api/collection?id=urn:cts:…` | `https://distributed-text-services.github.io/specifications/` | +| DOI | `https://doi.org/10.5281/zenodo.7702622` | `https://www.doi.org/` | +| ARK | `https://n2t.net/ark:/12148/btv1b8451636f` | `https://arks.org/` | +| Handle | `https://hdl.handle.net/1887/4531` | `https://www.handle.net/` | +| PURL | `https://purl.org/dc/terms/` | `https://purl.archive.org/` | +| URN:NBN | `urn:nbn:de:bvb:12-bsb00012345-2` | `https://nbn-resolving.org/` | +| Wikidata | `https://www.wikidata.org/entity/Q42` | `https://www.wikidata.org/` | TextRefs keeps mappings focused on textual resources. Identifiers of agents, organisations, instruments, or non-textual datasets (e.g. ROR, ORCID, ISNI) belong in external authority systems reached through mapped textual resources, not in `MappingAssertion.target`. diff --git a/src/content/docs/standard/system-profiles.md b/src/content/docs/standard/system-profiles.md index 4abeeb5..2f8717d 100644 --- a/src/content/docs/standard/system-profiles.md +++ b/src/content/docs/standard/system-profiles.md @@ -6,7 +6,7 @@ sidebar: order: 4 --- -Citation-system profiles constrain locator syntax through regular expressions and, where needed, additional documented validation rules. A pull request that adds or changes a citation system MUST include the profile record, examples of valid locators, and examples of invalid locators. +Citation-system profiles constrain locator syntax through regular expressions and, where needed, additional documented validation rules. A pull request that adds or changes a citation system MUST include the full profile record. ## Required profile fields @@ -14,14 +14,21 @@ Citation-system profiles constrain locator syntax through regular expressions an - `key`: flat stable key used for deterministic UUID seeds. - `type`: `CitationSystem`. - `preferred_label`: human-readable label. -- `normalization_version`: SemVer version. +- `description`: prose description of the citation tradition and its canonical locator form. - `locator_regex`: ECMAScript regular expression for machine-checkable locator pre-validation. -- `examples.valid`: locator examples that MUST match. -- `examples.invalid`: locator examples that MUST NOT match. See [Specification §7](/standard/specification/#7-citationsystem) for the full normative field list. -Profiles MUST follow the flat key and locator Unicode rules in [Identifier syntax](/standard/identifier-syntax/). A profile MAY add stricter locator rules for case, digits, punctuation, whitespace, allowed scripts, or non-regex-checkable constraints. Regex-checkable constraints MUST be reflected in examples and `locator_regex`; other constraints MUST be documented in the profile. The machine-actionable contract is the flat key, `normalization_version`, `locator_regex`, and examples. +## Canonical locator form + +Every profile defines exactly one canonical spelling for each reference point. Profiles MUST follow the flat key and locator Unicode rules in [Identifier syntax](/standard/identifier-syntax/). Along two ASCII axes the profile MUST take an explicit position: + +1. **Digit sequences.** The profile MUST state whether leading zeros are permitted in numeric components. Unless the citation tradition itself uses them, leading zeros MUST be forbidden. +2. **Letter case.** The profile MUST declare the canonical case of letter components. Locators are matched case-sensitively against that declared casing; case variants are non-canonical spellings. + +A profile MAY add further locator rules for punctuation, whitespace, allowed scripts, or non-regex-checkable constraints (such as a pinned book vocabulary). Regex-checkable constraints — including the two axes above — SHOULD be encoded in `locator_regex`; other constraints MUST be documented in `description`. The machine-actionable contract is the flat key and `locator_regex`. + +Because the canonical locator seeds the deterministic reference UUID, alternative spellings of the same reference point (`John.3.16` vs `john.3.16`, `514a1` vs `514a01`) would mint distinct permanent identities. Validators MUST reject non-canonical spellings; they MUST NOT fold them into the canonical form. The enumerated reference data in the registry is the authority on which locators are canonical and attested; `locator_regex` is the machine-checkable floor beneath it. ## Seed profiles @@ -30,12 +37,8 @@ Bekker profile (Aristotelian corpus). Implements **Bekker numbering**, the page- ```json { "key": "bekker", - "normalization_version": "1.0.0", - "locator_regex": "^[0-9]{3,4}[ab][0-9]{1,2}$", - "examples": { - "valid": ["983b10", "1003a21"], - "invalid": ["983", "983c10", "983b"] - } + "description": "Bekker numbering: page, column (a or b), and line, after August Immanuel Bekker's 1831 Berlin edition. Pages span the whole corpus from 1 (Categories 1a1) to roughly 1462. Canonical form: page and line are positive integers without leading zeros; the column letter is lowercase.", + "locator_regex": "^[1-9][0-9]{0,3}[ab][1-9][0-9]?$" } ``` @@ -44,15 +47,11 @@ Stephanus profile (Platonic corpus). Implements **Stephanus pagination**, the pa ```json { "key": "stephanus", - "normalization_version": "1.0.0", - "locator_regex": "^[0-9]{1,4}[a-e](?:[0-9]{1,2})?$", - "examples": { - "valid": ["514a", "514a1"], - "invalid": ["514f", "514", "514a100"] - } + "description": "Stephanus pagination: page, section (a–e), and optional sub-line, after Henri Estienne's 1578 edition of Plato's works. Canonical form: page and sub-line are positive integers without leading zeros; the section letter is lowercase.", + "locator_regex": "^[1-9][0-9]{0,3}[a-e](?:[1-9][0-9]?)?$" } ``` ## Validation rule -Every `CanonicalReference` MUST point to a known `CitationSystem`. Its `locator` MUST match that system's `locator_regex`, and its `normalization_version` MUST be the value fixed when the reference was minted (see [Specification §8](/standard/specification/#8-canonicalreference)); it need not equal the system's current `normalization_version`. Regex success is necessary but not sufficient: a usable TextRefs reference must resolve to a registered `CanonicalReference` and satisfy any additional profile validation rules. +Every `CanonicalReference` MUST point to a known `CitationSystem`. Its `locator` MUST be the profile's canonical spelling and MUST match that system's `locator_regex` (see [Specification §8](/standard/specification/#8-canonicalreference)). Regex success is necessary but not sufficient: a usable TextRefs reference must resolve to a registered `CanonicalReference` and satisfy any additional profile validation rules. diff --git a/src/content/docs/standard/versioning.md b/src/content/docs/standard/versioning.md index 146514e..a17a26a 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`. @@ -51,6 +53,7 @@ dist/dump/works.jsonl dist/dump/citation-systems.jsonl dist/dump/references.jsonl dist/dump/mappings.jsonl +dist/dump/aliases.json ``` Registry exports are organized by object type. This gives consumers stable file names, simple streaming imports, and one predictable place to find each record type. Resolver targets are embedded in reference records. Relationships are represented inside records through standard fields such as `key`, `work_key`, `citation_system_key`, `subject`, and `target`. @@ -67,28 +70,35 @@ Each `datapackage.json` MUST include: - `name`: `textrefs-registry`. - `version`: SemVer package version. - `licenses`: SPDX identifier `CC0-1.0` for registry data. -- `resources`: one resource per JSONL file. -- `schema`: field descriptors for each resource. +- `resources`: one resource per published file, that is, each JSONL record file and the JSON alias table. Each resource carries `path`, `format`, `mediatype`, `encoding`, `bytes`, and a `sha256:` `hash` of the resource body. + +Resources SHOULD additionally carry a Frictionless `schema` with field descriptors. Exports through `v0.1.0` omit it: record shapes are normative in the [specification](/standard/specification/) and enforced by the compiler, and restating them as Frictionless descriptors is [tracked separately](https://github.com/textrefs/textrefs.org/issues/74). Validate against the published shapes, not against the data package alone. ## Per-record versioning -Records do **not** carry their own SemVer. The registry is append-only with status transitions (`candidate` → `active` → `deprecated` / `withdrawn` / `blocked`). Consumers pin to a registry tag (or its DOI) for reproducibility. Identifier-level changes are expressed via tombstones, below. +Records do **not** carry their own SemVer. The registry is append-only from promotion onward, with status transitions (`draft` → `active` → `deprecated` / `withdrawn` / `blocked`). The `draft` tier is pre-persistence: see [Draft records and retraction](#draft-records-and-retraction). Consumers pin to a registry tag (or its DOI) for reproducibility. Identifier-level changes to promoted records are expressed via tombstones, below. + +## Draft records and retraction + +Records at status `draft` have not been promoted and carry no persistence promise ([Specification §11](/standard/specification/#11-identifier-policy)). A draft MAY be corrected — changing an identity field mints a different id, and the previous IRI ceases to resolve — or retracted, meaning the record is deleted outright. Retraction MUST NOT create a tombstone. Because identifiers are deterministic, a retracted tuple that is later re-proposed regains the same UUID; a reappearing id does not imply continuity of curation history. + +Draft records appear in exports inside the same `.jsonl` files as their type, with `status` as the signal — the same convention tombstones use. Consumers MUST NOT rely on a draft record persisting across releases and SHOULD filter on `status` when they need only promoted records. Rendered draft pages SHOULD be clearly flagged and excluded from search indexing. ## Tombstones and re-minted records -Registry identity is permanent: the IRI of a `Work`, `CitationSystem`, `CanonicalReference`, or `MappingAssertion` MUST continue to resolve once minted. Re-minting (renaming a key, correcting a locator that changes the content-derived UUID, splitting/merging records) MUST be expressed by **tombstoning** the old record and minting a successor. +Registry identity is permanent once promoted: the IRI of a `Work`, `CitationSystem`, `CanonicalReference`, or `MappingAssertion` MUST continue to resolve once the record has been published at status `active`. Re-minting a promoted record (renaming a key, correcting a locator that changes the content-derived UUID, splitting/merging records) MUST be expressed by **tombstoning** the old record and minting a successor. ### Schema -Tombstones use one status value, no extra fields. The old record stays in the data tree with `status: withdrawn`. If a successor exists, a single `MappingAssertion` with `relation: exactMatch`, `subject: `, and `target: ` carries the link. Consumers walk the mapping to find the successor. +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 -Tombstones are full records, not deletions. The old record retains every other field unchanged; only `status` flips to `withdrawn` and `modified` is bumped. If a successor exists, the successor is a separately authored record at the new IRI, and the linking `MappingAssertion` is committed alongside. +Tombstones are full records, not deletions. The old record retains every other field unchanged; `status` flips to `withdrawn`, `modified` is bumped, and `superseded_by` is set when a successor exists. The successor is a separately authored record at the new IRI. ### HTTP behavior -Old IRI HTML pages render a tombstone banner. The page already lists every `MappingAssertion` whose subject is this record, so the successor (if any) appears in that list with no extra rendering logic. The `.json` JSON-LD sibling returns the withdrawn record verbatim. Old IRIs are **not** hard-redirected: archival consumers MUST be able to inspect the tombstone payload. +Old IRI HTML pages render a tombstone banner; when `superseded_by` is present the banner links the successor IRI. The `.json` JSON-LD sibling returns the withdrawn record verbatim. Old IRIs are **not** hard-redirected: archival consumers MUST be able to inspect the tombstone payload. ### Export inclusion @@ -96,12 +106,27 @@ Tombstones MUST appear in monthly exports inside the same `.jsonl` file as their ### Compiler invariants -The compiler enforces: an active `CanonicalReference` MUST NOT reference a tombstoned `Work` or `CitationSystem` through `work_key` or `citation_system_key`. `MappingAssertion`s are exempt — successor links from a withdrawn subject to an active target are exactly the documented pattern. +The compiler enforces these invariants, and fails the build on any violation: + +1. `superseded_by` MUST only appear on records whose `status` is `deprecated`, `withdrawn`, or `blocked`. A record still in active use has no successor. +2. A `CanonicalReference` that is not itself a tombstone MUST NOT reference a tombstoned (`withdrawn` or `blocked`) `Work` or `CitationSystem` through `work_key` or `citation_system_key` — those break resolution. +3. `Work.preferred_citation_system_key` MUST name a known `CitationSystem`. +4. An `active` `Work` MUST have an `active` preferred `CitationSystem`. Its other citation systems MAY be `draft`; a fallback system's status never downgrades the work. +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 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 -`aliases.json` handles **presentational** URL aliases (multiple paths pointing at the same canonical record). Tombstones handle **identity** changes (the record itself is no longer canonical). These are distinct mechanisms and MUST NOT be conflated. +The compiler maintains a presentational alias map: multiple lookup paths pointing at the same canonical record. It publishes that map at `/dump/aliases.json`, and one per-work slice of it at `/reg/work/{work_key}/aliases.json`. Publishing the map does not change the permanence rules below. + +- External identifiers. +- `{work_key}/{citation_system_key}/{locator}` — a qualified alias, minted for every reference. +- `{work_key}/{locator}` — a bare alias, minted only for a work's preferred citation system ([Specification §6](/standard/specification/#6-work)). + +Tombstones handle **identity** changes; aliases handle **presentation**, and the two MUST NOT be conflated. Bare aliases MAY be added, removed, or **retargeted**: changing a work's preferred citation system changes what its bare alias resolves to, including for active works. Qualified aliases and `/id/ref/{uuid}` identifiers are never retargeted. ## Rights and content guardrails -Exports MUST NOT contain primary full text, commentary, apparatus, or rights metadata that implies TextRefs may redistribute copyrighted text. Disputed resolver endpoints remain in exports with `status: blocked` and tombstone rationale fields. +Exports MUST NOT contain primary full text, commentary, apparatus, or rights metadata that implies TextRefs may redistribute copyrighted text. Disputed resolver endpoints remain in exports with `status: blocked`. diff --git a/src/layouts/RecordPage.astro b/src/layouts/RecordPage.astro new file mode 100644 index 0000000..ee401e8 --- /dev/null +++ b/src/layouts/RecordPage.astro @@ -0,0 +1,44 @@ +--- +// Minimal standalone document for high-volume registry record pages +// (/id/ref/…). Deliberately not a Starlight page: 39k+ instances make +// per-page chrome the dominant cost of the deployed site. Styles live in +// the shared /assets/record.css (never inlined or bundled). +interface Props { + title: string; + description: string; + /** Omitted by a page with no single JSON-LD sibling, such as `/find/`. */ + jsonHref?: string; + noindex?: boolean; +} + +const { title, description, jsonHref, noindex = false } = Astro.props; +--- + + + + + + + {title} — TextRefs + + + + { + jsonHref && ( + + ) + } + {noindex && } + + + +
    + +
    + + diff --git a/src/lib/alias-index.test.ts b/src/lib/alias-index.test.ts new file mode 100644 index 0000000..9671795 --- /dev/null +++ b/src/lib/alias-index.test.ts @@ -0,0 +1,150 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { buildWorkAliasIndexes } from './alias-index.js'; +import { uuidOf } from './registry.js'; +import { fixtureRegistry } from './registry.fixture.js'; + +const { works, references } = fixtureRegistry; +const indexes = () => buildWorkAliasIndexes(works, references); + +// A synthetic reference, for the cases the fixture cannot express. +const ref = (citation_system_key: string, locator: string, uuid: string) => ({ + id: `https://textrefs.org/id/ref/${uuid}`, + work_key: 'fixture.work', + citation_system_key, + locator, +}); + +test('every reference of a work is reachable by system and locator', () => { + const [index] = indexes(); + assert.equal(index.work_key, 'fixture.work'); + for (const r of references) { + assert.ok( + index.refs[r.citation_system_key]?.[r.locator], + `unreachable: ${r.citation_system_key}/${r.locator}`, + ); + } +}); + +test('each mapped value is the reference UUID, not its IRI', () => { + const [index] = indexes(); + for (const r of references) { + const mapped = index.refs[r.citation_system_key][r.locator]; + assert.equal(mapped, uuidOf(r.id)); + assert.ok(!mapped.includes('/'), 'a UUID carries no path separator'); + } +}); + +test('the same locator under two systems keeps two identities', () => { + // ADR-0005: a work under two systems can repeat a locator string and mean a + // different passage each time. + const [index] = indexes(); + const a = index.refs['fixture-section']['1']; + const b = index.refs['fixture-alternate']['1']; + assert.ok(a && b); + assert.notEqual(a, b); +}); + +test('the index covers every reference exactly once', () => { + const mapped = indexes().reduce( + (total, index) => + total + + Object.values(index.refs).reduce( + (n, locators) => n + Object.keys(locators).length, + 0, + ), + 0, + ); + assert.equal(mapped, references.length); +}); + +test('a work with no references gets an index with an empty refs object', () => { + const [index] = buildWorkAliasIndexes(works, []); + assert.equal(index.work_key, 'fixture.work'); + assert.deepEqual(index.refs, {}); +}); + +test('the index names the preferred citation system', () => { + const [index] = indexes(); + assert.equal(index.preferred_citation_system_key, 'fixture-section'); + // The preferred key is presentation, not identity: both systems appear in + // `refs` with equal standing. + assert.deepEqual(Object.keys(index.refs).sort(), [ + 'fixture-alternate', + 'fixture-section', + ]); +}); + +test('references of another work do not leak in', () => { + const foreign = { + id: 'https://textrefs.org/id/ref/00000000-0000-5000-8000-0000000000ff', + work_key: 'other.work', + citation_system_key: 'fixture-section', + locator: '99', + }; + const built = buildWorkAliasIndexes(works, [...references, foreign]); + assert.equal(built.length, works.length); + assert.equal(built[0].refs['fixture-section']['99'], undefined); +}); + +test('draft references are indexed — the index makes no status claim', () => { + const draft = references.find((r) => r.status === 'draft'); + assert.ok(draft, 'the fixture carries a draft reference'); + const [index] = indexes(); + assert.equal( + index.refs[draft.citation_system_key][draft.locator], + uuidOf(draft.id), + ); +}); + +test('system keys sort, and locators sort numerically', () => { + const [index] = buildWorkAliasIndexes(works, [ + ref('fixture-section', '1.10', '00000000-0000-5000-8000-00000000000a'), + ref('fixture-section', '1.2', '00000000-0000-5000-8000-00000000000b'), + ref('fixture-section', '1.1', '00000000-0000-5000-8000-00000000000c'), + ref('fixture-alternate', '1', '00000000-0000-5000-8000-00000000000d'), + ]); + assert.deepEqual(Object.keys(index.refs), [ + 'fixture-alternate', + 'fixture-section', + ]); + assert.deepEqual(Object.keys(index.refs['fixture-section']), [ + '1.1', + '1.2', + '1.10', + ]); +}); + +test('the output does not depend on the input order', () => { + const forward = buildWorkAliasIndexes(works, references); + const backward = buildWorkAliasIndexes(works, [...references].reverse()); + assert.deepEqual(backward, forward); +}); + +test('a locator spelled __proto__ becomes an own key', () => { + const [index] = buildWorkAliasIndexes(works, [ + ref('fixture-section', '__proto__', '00000000-0000-5000-8000-00000000000e'), + ]); + const locators = index.refs['fixture-section']; + assert.ok(Object.hasOwn(locators, '__proto__')); + assert.equal(Object.getPrototypeOf(locators), Object.prototype); + assert.equal( + JSON.parse(JSON.stringify(locators))['__proto__'], + '00000000-0000-5000-8000-00000000000e', + ); +}); + +test('the caller’s arrays are not mutated', () => { + // `loadReferences()` hands back the live cached array. + const order = references.map((r) => r.id); + const keys = works.map((w) => w.key); + buildWorkAliasIndexes(works, references); + assert.deepEqual( + references.map((r) => r.id), + order, + ); + assert.deepEqual( + works.map((w) => w.key), + keys, + ); +}); diff --git a/src/lib/alias-index.ts b/src/lib/alias-index.ts new file mode 100644 index 0000000..f2b2ffa --- /dev/null +++ b/src/lib/alias-index.ts @@ -0,0 +1,96 @@ +// Per-work locator indexes served at `/reg/work/{key}/aliases.json`. +// +// The only public handle on a reference is its UUIDv5, seeded from the work +// key, the citation system key, and the locator (ADR-0002). A client that knows +// a passage but ships no UUIDv5 implementation could reach the record only +// through the `/cite/` redirect, which is HTML. This module turns the registry +// inside out into one JSON map per work, so the same lookup needs a JSON parse +// alone (#84). +// +// The indexes come from the reference records, not from `loadAliases()`. The +// flat alias table mixes two target kinds — a `/cite/` alias targets a +// reference IRI, an external mapping identifier targets a work IRI — and the +// records already carry the three fields the index groups by. +// +// The logic sits here rather than in the page because the repository has no +// Astro component test harness: `src/lib/*.test.ts` is where registry-shaped +// logic is pinned. + +import { uuidOf } from './registry.js'; + +// Locators sort the way the reference browser lists them — `1.2` before `1.10`. +// The locale is pinned: the bare `localeCompare(undefined, …)` the Astro pages +// use resolves against the host's ICU build, and a published file must not +// depend on which machine built it. +const byLocator = new Intl.Collator('en', { numeric: true }).compare; + +// Flat keys are ASCII, so code-unit order needs no collator and cannot drift. +const byCodeUnit = (a: string, b: string): number => + a < b ? -1 : a > b ? 1 : 0; + +export interface WorkAliasIndex { + work_key: string; + preferred_citation_system_key: string; + /** Citation system key → locator → CanonicalReference UUID. */ + refs: Record>; +} + +type IndexedWork = { key: string; preferred_citation_system_key: string }; +type IndexedReference = { + id: string; + work_key: string; + citation_system_key: string; + locator: string; +}; + +/** + * One index per work, in the order the works arrive. + * + * The map loses nothing, because the compiler makes `(work, system, locator)` + * unique: a block deduplicates its own locators, and a work cannot declare the + * same citation system twice. + */ +export function buildWorkAliasIndexes( + works: readonly IndexedWork[], + references: readonly IndexedReference[], +): WorkAliasIndex[] { + const byWork = new Map(); + for (const ref of references) { + const group = byWork.get(ref.work_key); + if (group) group.push(ref); + else byWork.set(ref.work_key, [ref]); + } + + return works.map((work) => { + const bySystem = new Map(); + for (const ref of byWork.get(work.key) ?? []) { + const group = bySystem.get(ref.citation_system_key); + if (group) group.push(ref); + else bySystem.set(ref.citation_system_key, [ref]); + } + + const refs: Record> = {}; + for (const [systemKey, group] of [...bySystem].sort(([a], [b]) => + byCodeUnit(a, b), + )) { + // Sorting `group` mutates an array this function built, never the + // caller's array. + group.sort((a, b) => byLocator(a.locator, b.locator)); + // `Object.fromEntries`, not assignment: a locator is author-controlled, + // and one spelled `__proto__` would set a prototype instead of a key. + // `fromEntries` defines own properties, so it cannot. + refs[systemKey] = Object.fromEntries( + group.map((ref) => [ref.locator, uuidOf(ref.id)]), + ); + } + + // Every work gets an index, including a work with no references. A client + // that holds a work key must be able to tell "no references yet" from + // "wrong key", and only a 200 with an empty `refs` does that. + return { + work_key: work.key, + preferred_citation_system_key: work.preferred_citation_system_key, + refs, + }; + }); +} diff --git a/src/lib/citation.ts b/src/lib/citation.ts index 9900620..75714aa 100644 --- a/src/lib/citation.ts +++ b/src/lib/citation.ts @@ -1,9 +1,15 @@ -import type { - Work, - Creator, - CanonicalReference, - CitationSystem, -} from '../../standard/schema/index.js'; +import type { Creator } from '../../standard/schema/index.js'; + +// The parameters below name the fields this module actually reads, rather than +// the whole compiled records. A full `Work`, `CanonicalReference`, or +// `CitationSystem` still satisfies them, so the record pages pass theirs +// unchanged; `/find/` passes the subset that `/reg/works.json` gives a browser. +export type CitedWork = { + preferred_label: string; + creators?: readonly Creator[]; +}; +export type CitedReference = { id: string; work_key: string; locator: string }; +export type CitedSystem = { preferred_label: string }; export type CSLName = { family: string; given?: string } | { literal: string }; @@ -27,9 +33,9 @@ function cslAuthor(c: Creator): CSLName { } export function toCSL( - work: Work | undefined, - ref: CanonicalReference, - system: CitationSystem | undefined, + work: CitedWork | undefined, + ref: CitedReference, + system: CitedSystem | undefined, ): CSLItem { const title = work ? work.preferred_label : ref.work_key; const authors = (work?.creators ?? []).map(cslAuthor); @@ -55,13 +61,26 @@ function authorsChicago(authors: CSLName[]): string { return `${names.slice(0, -1).join(', ')}, and ${names[names.length - 1]}`; } -export function formatChicago(csl: CSLItem): string { +/** + * `includeSection` names the citation system alongside the locator. Set it when + * the work is cited under more than one system, where a bare locator is + * ambiguous — the same string can denote a different passage per system + * (ADR-0005). Left off, the citation stays uncluttered for the common case. + */ +export function formatChicago( + csl: CSLItem, + opts: { includeSection?: boolean } = {}, +): string { const parts: string[] = []; const authors = csl.author ?? []; if (authors.length) parts.push(authorsChicago(authors)); parts.push(`*${csl.title}*`); const head = parts.join(', '); - return `${head} ${csl.locator}. ${csl.URL}`; + const locator = + opts.includeSection && csl.section + ? `${csl.locator} (${csl.section})` + : csl.locator; + return `${head} ${locator}. ${csl.URL}`; } export function toCOinS(csl: CSLItem): string { diff --git a/src/lib/collection.test.ts b/src/lib/collection.test.ts new file mode 100644 index 0000000..dc7bbb7 --- /dev/null +++ b/src/lib/collection.test.ts @@ -0,0 +1,38 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { collectionBody, JSONLD_CONTEXT } from './collection.js'; +import { fixtureRegistry } from './registry.fixture.js'; + +test('collectionBody sets the shared JSON-LD context', () => { + const body = collectionBody(fixtureRegistry.systems); + assert.equal(body['@context'], 'https://textrefs.org/contexts/v1.jsonld'); + assert.equal(body['@context'], JSONLD_CONTEXT); +}); + +test('collectionBody sorts @graph by key', () => { + const unsorted = [...fixtureRegistry.systems].reverse(); + assert.ok(unsorted.length > 1); + const body = collectionBody(unsorted); + const keys = body['@graph'].map((r) => r.key); + const expected = [...keys].sort((a, b) => a.localeCompare(b)); + assert.deepEqual(keys, expected); +}); + +test('collectionBody does not mutate the input array', () => { + const unsorted = [...fixtureRegistry.systems].reverse(); + const before = unsorted.map((r) => r.key); + collectionBody(unsorted); + assert.deepEqual( + unsorted.map((r) => r.key), + before, + ); +}); + +test('collectionBody passes items through unchanged with no per-item @context', () => { + const body = collectionBody(fixtureRegistry.systems); + const item = body['@graph'].find( + (r) => r.key === fixtureRegistry.systems[0].key, + ); + assert.deepEqual(item, fixtureRegistry.systems[0]); + assert.equal(item && '@context' in item, false); +}); diff --git a/src/lib/collection.ts b/src/lib/collection.ts new file mode 100644 index 0000000..1a10f05 --- /dev/null +++ b/src/lib/collection.ts @@ -0,0 +1,15 @@ +// Registry-wide JSON-LD collections served under `/reg/` (works and citation +// systems). The sort order here is shared with the HTML registry browser +// (`src/pages/reg/index.astro`), so the two views cannot drift apart. + +export const JSONLD_CONTEXT = 'https://textrefs.org/contexts/v1.jsonld'; + +export function byKey(a: { key: string }, b: { key: string }): number { + return a.key.localeCompare(b.key); +} + +export function collectionBody( + records: readonly T[], +) { + return { '@context': JSONLD_CONTEXT, '@graph': [...records].sort(byKey) }; +} diff --git a/src/lib/find.test.ts b/src/lib/find.test.ts new file mode 100644 index 0000000..60bc79d --- /dev/null +++ b/src/lib/find.test.ts @@ -0,0 +1,490 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { + TIER, + acceptsLocator, + boundedDistance, + interpret, + leadingTier, + normalizeLabel, + parseQuery, + rankWorks, + resolveInIndex, + type FindSystem, + type FindWork, + type Interpretation, +} from './find.js'; +import { buildWorkAliasIndexes } from './alias-index.js'; +import { fixtureRegistry } from './registry.fixture.js'; + +// A trimmed copy of the real registry. The locator regexes are verbatim from +// `/reg/systems.json`, so a locator that passes here passes in the browser. +const systems: FindSystem[] = [ + { + key: 'bekker', + preferred_label: 'Bekker numbering (Aristotelian corpus)', + description: + 'Bekker numbering: page, column (a or b), and line, e.g. 1094a1.', + locator_regex: + '^(?[1-9][0-9]{0,3})(?[ab])(?[1-9][0-9]?)$', + }, + { + key: 'bible-book-chapter-verse', + preferred_label: 'Bible book-chapter-verse (OSIS)', + description: 'OSIS locator: `Book.Chapter.Verse`, e.g. Gen.1.1.', + locator_regex: + '^(?[1-4]?[A-Za-z][A-Za-z0-9]*)\\.(?[1-9][0-9]*)\\.(?[1-9][0-9]*)$', + }, + { + key: 'homer-book-line', + preferred_label: 'Homeric book-line', + description: 'Book and line within it, as `book.line`, e.g. 1.1.', + locator_regex: '^(?[1-9]|1[0-9]|2[0-4])\\.(?[1-9][0-9]*)$', + }, + { + key: 'integer-section', + preferred_label: 'Integer section numbering', + description: 'Generic integer section locators, e.g. 43.', + locator_regex: '^(?
    [1-9][0-9]*)$', + }, + { + key: 'stephanus', + preferred_label: 'Stephanus pagination (Platonic corpus)', + description: + 'Stephanus pagination: page, section (a-e), optional sub-line, e.g. 514a.', + locator_regex: + '^(?[1-9][0-9]{0,3})(?
    [a-e])(?[1-9][0-9]?)?$', + }, + { + key: 'tractatus-proposition', + preferred_label: 'Tractatus proposition numbering', + description: 'Proposition numbers such as 1, 2.0121, 6.54, 7.', + locator_regex: '^(?[1-7](?:\\.[0-9]{1,5})?)$', + }, +]; + +const work = ( + key: string, + preferred_label: string, + preferred_citation_system_key: string, + creators: FindWork['creators'] = undefined, + alternative_labels?: string[], +): FindWork => ({ + key, + preferred_label, + preferred_citation_system_key, + status: 'draft', + ...(creators ? { creators } : {}), + ...(alternative_labels ? { alternative_labels } : {}), +}); + +const person = (family: string, given?: string): FindWork['creators'] => [ + given ? { kind: 'person', family, given } : { kind: 'person', family }, +]; + +const works: FindWork[] = [ + work( + 'aristotle.nicomachean-ethics', + 'Nicomachean Ethics', + 'bekker', + person('Aristotle'), + ), + work('homer.iliad', 'Iliad', 'homer-book-line', person('Homer')), + work('homer.odyssey', 'Odyssey', 'homer-book-line', person('Homer')), + work('plato.republic', 'Republic', 'stephanus', person('Plato')), + work('tanakh', 'Tanakh', 'bible-book-chapter-verse'), + work( + 'wittgenstein.philosophical-investigations', + 'Philosophical Investigations', + 'integer-section', + person('Wittgenstein', 'Ludwig'), + ), + work( + 'wittgenstein.tractatus', + 'Tractatus Logico-Philosophicus', + 'tractatus-proposition', + person('Wittgenstein', 'Ludwig'), + ), +]; + +const find = (query: string, over: FindWork[] = works): Interpretation => + interpret(query, over, systems); + +// --------------------------------------------------------------------------- +// The minimum cases the technical spec names +// --------------------------------------------------------------------------- + +test('"Plato Republic" identifies one work and waits for a locator', () => { + const result = find('Plato Republic'); + assert.equal(result.kind, 'work-selected'); + assert.equal( + result.kind === 'work-selected' && result.match.work.key, + 'plato.republic', + ); + assert.equal( + result.kind === 'work-selected' && result.match.tier, + TIER.CREATOR_AND_TITLE, + ); +}); + +test('"Plato Republic 514a" is resolvable against one work and locator', () => { + const result = find('Plato Republic 514a'); + assert.equal(result.kind, 'resolvable'); + if (result.kind !== 'resolvable') return; + assert.equal(result.match.work.key, 'plato.republic'); + assert.equal(result.locator, '514a'); +}); + +test('"Republic 514a" resolves without the creator', () => { + const result = find('Republic 514a'); + assert.equal(result.kind, 'resolvable'); + if (result.kind !== 'resolvable') return; + assert.equal(result.match.work.key, 'plato.republic'); + assert.equal(result.match.tier, TIER.EXACT_LABEL); + assert.equal(result.locator, '514a'); +}); + +test('"Nicomachean Ethics 1094a1" resolves under Bekker numbering', () => { + const result = find('Nicomachean Ethics 1094a1'); + assert.equal(result.kind, 'resolvable'); + if (result.kind !== 'resolvable') return; + assert.equal(result.match.work.key, 'aristotle.nicomachean-ethics'); + assert.equal(result.locator, '1094a1'); +}); + +test('a bare locator is never resolved, even with one candidate', () => { + // Republic is the only Stephanus work today, so the candidate list has one + // entry. The finder still refuses to pick it: the query named no work. + const result = find('514a'); + assert.equal(result.kind, 'bare-locator'); + if (result.kind !== 'bare-locator') return; + assert.equal(result.locator, '514a'); + assert.deepEqual( + result.candidates.map((c) => c.work.key), + ['plato.republic'], + ); +}); + +test('"unknown work" matches nothing', () => { + assert.equal(find('unknown work').kind, 'no-match'); +}); + +// --------------------------------------------------------------------------- +// Ambiguity is never resolved silently +// --------------------------------------------------------------------------- + +test('"Homer 1.1" stays ambiguous across the two Homeric works', () => { + const result = find('Homer 1.1'); + assert.equal(result.kind, 'work-matches'); + if (result.kind !== 'work-matches') return; + assert.deepEqual( + result.matches.map((m) => m.work.key), + ['homer.iliad', 'homer.odyssey'], + ); + // The locator survives, so choosing a work resolves in one more step. + assert.equal(result.locator, '1.1'); +}); + +test('two works sharing an alternative label are ambiguous (ADR-0007)', () => { + // ADR-0007 allows the homonym and requires the consumer to treat it as + // ambiguous. "Ethics" fits Aristotle and Spinoza. + const homonyms: FindWork[] = [ + work( + 'aristotle.nicomachean-ethics', + 'Nicomachean Ethics', + 'bekker', + person('Aristotle'), + ['NE', 'Ethics'], + ), + work('spinoza.ethica', 'Ethica', 'integer-section', person('Spinoza'), [ + 'Ethics', + ]), + ]; + const result = interpret('Ethics', homonyms, systems); + assert.equal(result.kind, 'work-matches'); + if (result.kind !== 'work-matches') return; + assert.equal(result.matches.length, 2); + assert.ok( + result.matches.every((m) => m.tier === TIER.EXACT_ALTERNATIVE_LABEL), + ); +}); + +test('an exact preferred label outranks another work’s alternative label', () => { + // The spec's ladder puts tier 2 above tier 3, so this is decided rather than + // ambiguous. Recorded here because it is the one homonym shape that does not + // end in a question. + const mixed: FindWork[] = [ + work( + 'aristotle.nicomachean-ethics', + 'Nicomachean Ethics', + 'bekker', + person('Aristotle'), + ['Ethics'], + ), + work('spinoza.ethics', 'Ethics', 'integer-section', person('Spinoza')), + ]; + const result = interpret('Ethics', mixed, systems); + assert.equal(result.kind, 'work-selected'); + assert.equal( + result.kind === 'work-selected' && result.match.work.key, + 'spinoza.ethics', + ); +}); + +// --------------------------------------------------------------------------- +// Locator shape +// --------------------------------------------------------------------------- + +test('a locator no citation system accepts is reported, not guessed', () => { + const result = find('Republic 999z'); + assert.equal(result.kind, 'locator-invalid'); + if (result.kind !== 'locator-invalid') return; + assert.equal(result.match.work.key, 'plato.republic'); + assert.equal(result.locator, '999z'); +}); + +test('a Stephanus work does not accept a bare integer locator', () => { + // "327" satisfies `integer-section`, so `parseQuery` calls it well formed + // registry-wide. Only the work's own index can reject it, which it does. + const result = find('Republic 327'); + assert.equal(result.kind, 'resolvable'); + + const index = { + work_key: 'plato.republic', + preferred_citation_system_key: 'stephanus', + refs: { stephanus: { '327a': 'cecef712-e8cf-5878-9d48-419f7d185a56' } }, + }; + const resolution = resolveInIndex(index, '327', systems); + assert.equal(resolution.kind, 'locator-invalid'); + if (resolution.kind !== 'locator-invalid') return; + assert.deepEqual(resolution.workSystems, ['stephanus']); +}); + +test('a leading number is not mistaken for a locator', () => { + const parsed = parseQuery('1 Corinthians', systems); + assert.equal(parsed.locator, null); + assert.equal(parsed.workQuery, '1 Corinthians'); +}); + +test('a trailing token without a digit is never a locator', () => { + assert.equal(parseQuery('Nicomachean Ethics', systems).locator, null); +}); + +test('an OSIS book code survives parsing as one token', () => { + const parsed = parseQuery('Tanakh Gen.1.1', systems); + assert.equal(parsed.locator, 'Gen.1.1'); + assert.equal(parsed.workQuery, 'Tanakh'); + assert.equal(parsed.locatorIsWellFormed, true); +}); + +test('locator case and dots are preserved verbatim (ADR-0002)', () => { + const result = find('Tanakh 1Sam.1.1'); + assert.equal(result.kind, 'resolvable'); + assert.equal(result.kind === 'resolvable' && result.locator, '1Sam.1.1'); +}); + +// --------------------------------------------------------------------------- +// Ranking +// --------------------------------------------------------------------------- + +test('an exact key outranks every title match', () => { + const matches = rankWorks('plato.republic', works); + assert.equal(matches[0].tier, TIER.EXACT_KEY); + assert.equal(matches[0].work.key, 'plato.republic'); + assert.deepEqual( + leadingTier(matches).map((m) => m.work.key), + ['plato.republic'], + ); +}); + +test('an alternative label finds a work the preferred label hides', () => { + // No work in `data/` carries `alternative_labels` yet, so this tier is + // proven here rather than against the registry (ADR-0007 follow-up). + const withAbbreviations = works.map((w) => + w.key === 'aristotle.nicomachean-ethics' + ? { ...w, alternative_labels: ['NE', 'EN', 'Nikomachische Ethik'] } + : w, + ); + const result = interpret('NE 1094a1', withAbbreviations, systems); + assert.equal(result.kind, 'resolvable'); + if (result.kind !== 'resolvable') return; + assert.equal(result.match.work.key, 'aristotle.nicomachean-ethics'); + assert.equal(result.match.tier, TIER.EXACT_ALTERNATIVE_LABEL); + assert.equal(result.match.matchedOn, 'NE'); +}); + +test('a translated title finds the work', () => { + const withAbbreviations = works.map((w) => + w.key === 'aristotle.nicomachean-ethics' + ? { ...w, alternative_labels: ['Nikomachische Ethik'] } + : w, + ); + const result = interpret('Nikomachische Ethik', withAbbreviations, systems); + assert.equal(result.kind, 'work-selected'); +}); + +test('a title prefix ranks above a bare token match', () => { + const matches = rankWorks('Tractatus', works); + assert.equal(matches[0].tier, TIER.TITLE_PREFIX); + assert.equal(matches[0].work.key, 'wittgenstein.tractatus'); +}); + +test('a creator name alone is a token match across that creator’s works', () => { + const matches = leadingTier(rankWorks('Wittgenstein', works)); + assert.deepEqual( + matches.map((m) => m.work.key), + ['wittgenstein.philosophical-investigations', 'wittgenstein.tractatus'], + ); + assert.ok(matches.every((m) => m.tier === TIER.TOKEN)); +}); + +test('a near miss is found by fuzzy matching', () => { + const matches = rankWorks('Republik', works); + assert.equal(matches[0].tier, TIER.FUZZY); + assert.equal(matches[0].work.key, 'plato.republic'); +}); + +test('fuzzy matching never outranks an exact hit', () => { + // "Odyssey" is an exact label and within edit distance of nothing else, but + // the guarantee is structural: the leading tier is a single tier. + const matches = leadingTier(rankWorks('Odyssey', works)); + assert.deepEqual( + matches.map((m) => m.work.key), + ['homer.odyssey'], + ); + assert.equal(matches[0].tier, TIER.EXACT_LABEL); +}); + +test('a short query is not fuzzy matched', () => { + assert.equal( + rankWorks('Ili', works).some((m) => m.tier === TIER.FUZZY), + false, + ); +}); + +test('ranking is deterministic', () => { + const once = rankWorks('Homer', works); + const twice = rankWorks('Homer', works); + assert.deepEqual( + once.map((m) => [m.work.key, m.tier]), + twice.map((m) => [m.work.key, m.tier]), + ); +}); + +test('punctuation and diacritics do not have to be typed', () => { + assert.equal( + normalizeLabel('Tractatus Logico-Philosophicus'), + 'tractatus logico philosophicus', + ); + assert.equal(normalizeLabel('Nikomachische Ethík'), 'nikomachische ethik'); + assert.equal(find('tractatus logico philosophicus').kind, 'work-selected'); +}); + +test('an empty query is the empty state', () => { + assert.equal(find('').kind, 'empty'); + assert.equal(find(' ').kind, 'empty'); +}); + +test('boundedDistance abandons past the limit', () => { + assert.equal(boundedDistance('republic', 'republik', 2), 1); + assert.equal(boundedDistance('republic', 'odyssey', 2), 3); + assert.ok(boundedDistance('a', 'abcdefgh', 2) > 2); +}); + +test('acceptsLocator rejects a system whose regex does not compile', () => { + assert.equal( + acceptsLocator( + { + key: 'broken', + preferred_label: 'Broken', + description: 'Does not compile.', + locator_regex: '^(', + }, + '1', + ), + false, + ); +}); + +// --------------------------------------------------------------------------- +// Stage two: the alias index decides identity +// --------------------------------------------------------------------------- + +const republicIndex = { + work_key: 'plato.republic', + preferred_citation_system_key: 'stephanus', + refs: { + stephanus: { + '327a': 'cecef712-e8cf-5878-9d48-419f7d185a56', + '514a': 'dc799d4b-9b17-5d76-85aa-dfd001c5321d', + }, + }, +}; + +test('a locator present under one system resolves to its UUID', () => { + const resolution = resolveInIndex(republicIndex, '514a', systems); + assert.equal(resolution.kind, 'resolved'); + if (resolution.kind !== 'resolved') return; + assert.equal(resolution.uuid, 'dc799d4b-9b17-5d76-85aa-dfd001c5321d'); + assert.equal(resolution.systemKey, 'stephanus'); +}); + +test('a well-formed locator absent from the index is not invented (ADR-0002)', () => { + // "999e" is a valid Stephanus string but Republic ends at 621d. There is no + // near miss and no fallback: identity comes from the index or not at all. + const resolution = resolveInIndex(republicIndex, '999e', systems); + assert.equal(resolution.kind, 'not-in-registry'); + if (resolution.kind !== 'not-in-registry') return; + assert.deepEqual(resolution.validUnder, ['stephanus']); +}); + +test('lookup is exact, never case folded', () => { + assert.notEqual( + resolveInIndex(republicIndex, '514A', systems).kind, + 'resolved', + ); +}); + +test('a locator spelled __proto__ cannot reach the prototype', () => { + const resolution = resolveInIndex(republicIndex, '__proto__', systems); + assert.notEqual(resolution.kind, 'resolved'); +}); + +test('one locator under two systems is ambiguous, not picked (ADR-0005)', () => { + // The build fixture declares a work under two citation systems that both + // carry locator "1"; the same string denotes a different passage in each. + const [index] = buildWorkAliasIndexes( + fixtureRegistry.works, + fixtureRegistry.references, + ); + const fixtureSystems: FindSystem[] = fixtureRegistry.systems.map((s) => ({ + key: s.key, + preferred_label: s.preferred_label, + description: s.description, + locator_regex: s.locator_regex, + })); + + const resolution = resolveInIndex(index, '1', fixtureSystems); + assert.equal(resolution.kind, 'ambiguous-system'); + if (resolution.kind !== 'ambiguous-system') return; + assert.equal(resolution.options.length, 2); + // The work's preferred system leads the list. + assert.equal( + resolution.options[0].systemKey, + index.preferred_citation_system_key, + ); + // Two systems, two identities. + assert.notEqual(resolution.options[0].uuid, resolution.options[1].uuid); +}); + +test('a work with no references reports the locator as unusable', () => { + const empty = { + work_key: 'empty.work', + preferred_citation_system_key: 'stephanus', + refs: {}, + }; + const resolution = resolveInIndex(empty, '514a', systems); + assert.equal(resolution.kind, 'locator-invalid'); + if (resolution.kind !== 'locator-invalid') return; + assert.deepEqual(resolution.workSystems, []); +}); diff --git a/src/lib/find.ts b/src/lib/find.ts new file mode 100644 index 0000000..b41b218 --- /dev/null +++ b/src/lib/find.ts @@ -0,0 +1,492 @@ +// Query interpretation for the reference finder at `/find/`. +// +// This module is pure. It holds no DOM reference, opens no network connection, +// and reads no registry file. Two reasons: +// +// 1. It ships to the browser. `src/pages/find/index.astro` imports it from a +// bundled ` + + diff --git a/src/pages/id/mapping/[uuid].json.ts b/src/pages/id/mapping/[uuid].json.ts index fa556eb..a592a66 100644 --- a/src/pages/id/mapping/[uuid].json.ts +++ b/src/pages/id/mapping/[uuid].json.ts @@ -16,6 +16,6 @@ export const GET: APIRoute = ({ props }) => { }; const body = { '@context': CONTEXT, ...mapping }; return new Response(JSON.stringify(body, null, 2), { - headers: { 'Content-Type': 'application/json; charset=utf-8' }, + headers: { 'Content-Type': 'application/ld+json; charset=utf-8' }, }); }; diff --git a/src/pages/id/mapping/[uuid]/index.astro b/src/pages/id/mapping/[uuid]/index.astro index 5dfa0b1..65e4320 100644 --- a/src/pages/id/mapping/[uuid]/index.astro +++ b/src/pages/id/mapping/[uuid]/index.astro @@ -35,25 +35,34 @@ const jsonHref = `/id/mapping/${uuidOf(mapping.id)}.json`; tag: 'link', attrs: { rel: 'alternate', - type: 'application/json', + type: 'application/ld+json', href: jsonHref, }, }, + // Draft records are excluded from search indexing (ADR-0003). + ...(mapping.status === 'draft' + ? [ + { + tag: 'meta' as const, + attrs: { name: 'robots', content: 'noindex' }, + }, + ] + : []), ], }} >

    - MappingAssertion - {mapping.status} - {mapping.id} + MappingAssertion{' '} + {mapping.status}{' '} + {mapping.id}{' '} JSON

    - + @@ -129,6 +173,10 @@ const jsonHref = `/id/work/${work.key}.json`;

    Aliases ({aliases.length})

    +

    + Every locator of this work maps to its reference UUID in the{' '} + locator index. +

    Mappings ({mappings.length})

    { @@ -140,10 +188,7 @@ const jsonHref = `/id/work/${work.key}.json`;
  • {m.relation} - - {m.target.target_kind && ( - {m.target.target_kind} - )} + {' '} {m.target.identifier}
  • ))} @@ -164,6 +209,11 @@ const jsonHref = `/id/work/${work.key}.json`; {r.locator} {' '} + {multiSystem && ( + <> + {r.citation_system_key}{' '} + + )} {r.resolver_targets.length} target(s) diff --git a/src/pages/reg/index.astro b/src/pages/reg/index.astro index e9aa101..9f35d53 100644 --- a/src/pages/reg/index.astro +++ b/src/pages/reg/index.astro @@ -2,9 +2,18 @@ import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro'; import { loadWorks, loadSystems } from '../../lib/registry.ts'; import { UNSTABLE_BANNER } from '../../lib/banner.ts'; +import { byKey } from '../../lib/collection.ts'; -const works = [...loadWorks()].sort((a, b) => a.key.localeCompare(b.key)); -const systems = [...loadSystems()].sort((a, b) => a.key.localeCompare(b.key)); +const works = [...loadWorks()].sort(byKey); +const systems = [...loadSystems()].sort(byKey); + +// The client filter below is a plain substring match on this attribute, so a +// work is found by its abbreviation and by a translated title as soon as those +// sit in the haystack (ADR-0007). +const workFilterKey = (w: (typeof works)[number]) => + [w.preferred_label, ...(w.alternative_labels ?? []), w.key] + .join(' ') + .toLowerCase(); --- a.key.localeCompare(b.key)); template: 'doc', tableOfContents: false, banner: UNSTABLE_BANNER, + head: [ + { + tag: 'link', + attrs: { + rel: 'alternate', + type: 'application/ld+json', + href: '/reg/works.json', + title: 'Works', + }, + }, + { + tag: 'link', + attrs: { + rel: 'alternate', + type: 'application/ld+json', + href: '/reg/systems.json', + title: 'Citation systems', + }, + }, + ], }} >

    The TextRefs registry holds works (texts that get cited), + {' '} citation systems (rules for how locators inside a work look), and canonical references (individual points inside a work). Each record lives at its persistent identifier under /id/; this page - is the human entry point for finding them. See - URL layout for how - /id/, /reg/, /cite/, and - /api/ fit together, or the + is the human entry point for finding them. See{' '} + URL layout for how{' '} + /id/, /reg/, /cite/, and{' '} + /api/ fit together, or the{' '} specification for the data model.

    @@ -46,7 +76,7 @@ const systems = [...loadSystems()].sort((a, b) => a.key.localeCompare(b.key)); ) : (
      {works.map((w) => ( -
    • +
    • {w.preferred_label}{' '} {w.status}{' '} {w.key} diff --git a/src/pages/reg/system/[key]/refs/[page].astro b/src/pages/reg/system/[key]/refs/[page].astro deleted file mode 100644 index 96cfa34..0000000 --- a/src/pages/reg/system/[key]/refs/[page].astro +++ /dev/null @@ -1,95 +0,0 @@ ---- -import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro'; -import { - loadSystems, - loadReferences, - uuidOf, -} from '../../../../../lib/registry.ts'; -import { UNSTABLE_BANNER } from '../../../../../lib/banner.ts'; - -export async function getStaticPaths() { - const PAGE_SIZE = 200; - const systems = loadSystems(); - const refs = loadReferences(); - const refsBySystem = new Map(); - for (const r of refs) { - const arr = refsBySystem.get(r.citation_system_key); - if (arr) arr.push(r); - else refsBySystem.set(r.citation_system_key, [r]); - } - return systems.flatMap((system) => { - const systemRefs = (refsBySystem.get(system.key) ?? []) - .slice() - .sort((a, b) => - a.locator.localeCompare(b.locator, undefined, { numeric: true }), - ); - const pageCount = Math.max(1, Math.ceil(systemRefs.length / PAGE_SIZE)); - return Array.from({ length: pageCount }, (_, i) => { - const page = i + 1; - const slice = systemRefs.slice(i * PAGE_SIZE, (i + 1) * PAGE_SIZE); - return { - params: { key: system.key, page: String(page) }, - props: { - system, - refs: slice, - page, - pageCount, - pageSize: PAGE_SIZE, - total: systemRefs.length, - }, - }; - }); - }); -} - -const { system, refs, page, pageCount, pageSize, total } = Astro.props; -const prev = page > 1 ? page - 1 : null; -const next = page < pageCount ? page + 1 : null; -const from = (page - 1) * pageSize + 1; -const to = Math.min(page * pageSize, total); ---- - - -

      - ← {system.preferred_label} -

      - -

      - Showing references {from}–{to} of {total} · page {page} of {pageCount}. -

      - - - - -
      diff --git a/src/pages/reg/systems.json.ts b/src/pages/reg/systems.json.ts new file mode 100644 index 0000000..592a28b --- /dev/null +++ b/src/pages/reg/systems.json.ts @@ -0,0 +1,8 @@ +import type { APIRoute } from 'astro'; +import { loadSystems } from '../../lib/registry.ts'; +import { collectionBody } from '../../lib/collection.ts'; + +export const GET: APIRoute = () => + new Response(JSON.stringify(collectionBody(loadSystems()), null, 2), { + headers: { 'Content-Type': 'application/ld+json; charset=utf-8' }, + }); diff --git a/src/pages/reg/work/[key]/aliases.json.ts b/src/pages/reg/work/[key]/aliases.json.ts new file mode 100644 index 0000000..be4dcb6 --- /dev/null +++ b/src/pages/reg/work/[key]/aliases.json.ts @@ -0,0 +1,25 @@ +import type { APIRoute, GetStaticPaths } from 'astro'; +import { loadWorks, loadReferences } from '../../../../lib/registry.ts'; +import { + buildWorkAliasIndexes, + type WorkAliasIndex, +} from '../../../../lib/alias-index.ts'; + +// Everything comes from the compiled registry in memory. `npm run build` runs +// `astro build` before `tsx scripts/compile.ts`, so `/dump/aliases.json` does +// not exist yet at this point and this route must never read it. +export const getStaticPaths: GetStaticPaths = () => + buildWorkAliasIndexes(loadWorks(), loadReferences()).map((index) => ({ + params: { key: index.work_key }, + props: { index }, + })); + +export const GET: APIRoute = ({ props }) => { + const { index } = props as { index: WorkAliasIndex }; + // Minified, unlike the `/reg/*.json` collections: the largest index holds + // 23k locators and no person reads this file. Plain JSON, not JSON-LD — it + // carries no `@context` and states identity alone. + return new Response(JSON.stringify(index), { + headers: { 'Content-Type': 'application/json; charset=utf-8' }, + }); +}; diff --git a/src/pages/reg/work/[key]/refs/[page].astro b/src/pages/reg/work/[key]/refs/[page].astro index 76c45c5..ebf3a98 100644 --- a/src/pages/reg/work/[key]/refs/[page].astro +++ b/src/pages/reg/work/[key]/refs/[page].astro @@ -20,9 +20,15 @@ export async function getStaticPaths() { return works.flatMap((work) => { const workRefs = (refsByWork.get(work.key) ?? []) .slice() - .sort((a, b) => - a.locator.localeCompare(b.locator, undefined, { numeric: true }), + .sort( + (a, b) => + a.citation_system_key.localeCompare(b.citation_system_key) || + a.locator.localeCompare(b.locator, undefined, { numeric: true }), ); + // A work cited under several systems can repeat a locator string while + // meaning a different passage each time (ADR-0005) — label them. + const multiSystem = + new Set(workRefs.map((r) => r.citation_system_key)).size > 1; const pageCount = Math.max(1, Math.ceil(workRefs.length / PAGE_SIZE)); return Array.from({ length: pageCount }, (_, i) => { const page = i + 1; @@ -32,6 +38,7 @@ export async function getStaticPaths() { props: { work, refs: slice, + multiSystem, page, pageCount, pageSize: PAGE_SIZE, @@ -42,7 +49,8 @@ export async function getStaticPaths() { }); } -const { work, refs, page, pageCount, pageSize, total } = Astro.props; +const { work, refs, multiSystem, page, pageCount, pageSize, total } = + Astro.props; const prev = page > 1 ? page - 1 : null; const next = page < pageCount ? page + 1 : null; const from = (page - 1) * pageSize + 1; @@ -57,10 +65,21 @@ const to = Math.min(page * pageSize, total); tableOfContents: false, pagefind: false, banner: UNSTABLE_BANNER, + // Draft records are excluded from search indexing (ADR-0003). + head: + work.status === 'draft' + ? [ + { + tag: 'meta' as const, + attrs: { name: 'robots', content: 'noindex' }, + }, + ] + : [], }} >

      - ← {work.preferred_label} + ← {work.preferred_label}{' '} + {work.status === 'draft' && draft}

      @@ -74,6 +93,11 @@ const to = Math.min(page * pageSize, total); {r.locator} {' '} + {multiSystem && ( + <> + {r.citation_system_key}{' '} + + )} {r.resolver_targets.length} target(s)

    • )) diff --git a/src/pages/reg/works.json.ts b/src/pages/reg/works.json.ts new file mode 100644 index 0000000..c3226f6 --- /dev/null +++ b/src/pages/reg/works.json.ts @@ -0,0 +1,8 @@ +import type { APIRoute } from 'astro'; +import { loadWorks } from '../../lib/registry.ts'; +import { collectionBody } from '../../lib/collection.ts'; + +export const GET: APIRoute = () => + new Response(JSON.stringify(collectionBody(loadWorks()), null, 2), { + headers: { 'Content-Type': 'application/ld+json; charset=utf-8' }, + }); diff --git a/standard/schema/canonical-reference.ts b/standard/schema/canonical-reference.ts index 42cba54..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, SemVer } 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']), @@ -24,7 +24,6 @@ export const CanonicalReferenceBase = AdminMetadata.extend({ work_key: FlatKey, citation_system_key: FlatKey, locator: z.string().min(1), - normalization_version: SemVer, resolver_targets: z.array(ResolverTargetEntry).default([]), }); diff --git a/standard/schema/citation-system.ts b/standard/schema/citation-system.ts index 805b70e..39acb3e 100644 --- a/standard/schema/citation-system.ts +++ b/standard/schema/citation-system.ts @@ -1,54 +1,32 @@ import { z } from 'zod'; -import { AdminMetadata, FlatKey, SemVer } from './common.js'; +import { AdminMetadata, FlatKey } from './common.js'; export const CitationSystemBase = AdminMetadata.extend({ id: z.string().regex(/^https:\/\/textrefs\.org\/id\/system\/[^/]+$/), key: FlatKey, type: z.literal('CitationSystem'), preferred_label: z.string().min(1), - normalization_version: SemVer, + description: z.string().min(1), locator_regex: z.string().min(1), - examples: z.object({ - valid: z.array(z.string()).min(1), - invalid: z.array(z.string()).min(1), - }), }); export const CitationSystem = CitationSystemBase.superRefine((s, ctx) => { if (s.id !== `https://textrefs.org/id/system/${s.key}`) { ctx.addIssue({ - code: z.ZodIssueCode.custom, + code: 'custom', message: 'id MUST be https://textrefs.org/id/system/{key}', path: ['id'], }); } - let re: RegExp; try { - re = new RegExp(s.locator_regex); + new RegExp(s.locator_regex); } catch { ctx.addIssue({ - code: z.ZodIssueCode.custom, + code: 'custom', message: 'locator_regex is not a valid ECMAScript regex', path: ['locator_regex'], }); - return; } - s.examples.valid.forEach((v, i) => { - if (!re.test(v)) - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `examples.valid[${i}] does not match locator_regex`, - path: ['examples', 'valid', i], - }); - }); - s.examples.invalid.forEach((v, i) => { - if (re.test(v)) - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `examples.invalid[${i}] must NOT match locator_regex`, - path: ['examples', 'invalid', i], - }); - }); }); export type CitationSystem = z.infer; diff --git a/standard/schema/common.ts b/standard/schema/common.ts index 6eed376..893fa92 100644 --- a/standard/schema/common.ts +++ b/standard/schema/common.ts @@ -1,7 +1,10 @@ import { z } from 'zod'; +// Lifecycle (ADR-0004): `draft` is mutable and carries no persistence promise; +// promotion to `active` grants both recommendation and identifier permanence. +// The remaining three are tombstone states for records that have left active use. export const Status = z.enum([ - 'candidate', + 'draft', 'active', 'deprecated', 'withdrawn', @@ -12,18 +15,32 @@ export const IsoDate = z .string() .regex(/^\d{4}-\d{2}-\d{2}$/, 'must be YYYY-MM-DD'); -export const Iri = z.string().url(); +export const Iri = z.url(); export const AdminMetadata = z.object({ status: Status, created: IsoDate, modified: IsoDate, + // Successor link (dcterms:isReplacedBy) on records that have left active + // use. The status constraint — deprecated, withdrawn, or blocked only — + // needs the whole registry, so the compiler enforces it, not this shape. + superseded_by: Iri.optional(), }); export const FlatKey = z .string() .regex(/^[a-z0-9][a-z0-9._-]*$/, 'flat key syntax: ^[a-z0-9][a-z0-9._-]*$'); -export const SemVer = z +// BCP 47 well-formedness (RFC 5646 langtag + privateuse). Registry validity is +// 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(/^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/); + .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/mapping-assertion.ts b/standard/schema/mapping-assertion.ts index be7a7a1..13709c1 100644 --- a/standard/schema/mapping-assertion.ts +++ b/standard/schema/mapping-assertion.ts @@ -18,10 +18,11 @@ export const MappingAssertionBase = AdminMetadata.extend({ ), type: z.literal('MappingAssertion'), subject: WorkIri, - relation: z.enum(['exactMatch', 'closeMatch']), + // ADR-0006. Kept in lockstep with MappingSource in scripts/source-schema.ts. + relation: z.enum(['alternateOf', 'isReferencedBy']), target: z.object({ - target_kind: z.string().optional(), identifier: Iri, + conforms_to: z.union([Iri, z.array(Iri).min(1)]).optional(), }), source: z.string().min(1), }); diff --git a/standard/schema/work.ts b/standard/schema/work.ts index 51dfb89..1a1c063 100644 --- a/standard/schema/work.ts +++ b/standard/schema/work.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { AdminMetadata, FlatKey } from './common.js'; +import { AdminMetadata, FlatKey, Iri } from './common.js'; export const Creator = z.discriminatedUnion('kind', [ z.object({ @@ -20,17 +20,59 @@ export const WorkBase = AdminMetadata.extend({ key: FlatKey, type: z.literal('Work'), preferred_label: z.string().min(1), + // Additional names for the work: abbreviations ("NE"), translated titles + // ("Nikomachische Ethik"), and established short forms. Search and display + // only. Identity-neutral: no label is a UUID seed input (ADR-0002), so a + // label change never moves an identifier (ADR-0007). Two works MAY share an + // alternative label; the compiler does not treat that as an error. Omit the + // field rather than publishing an empty array; the entries are checked for + // uniqueness below. + alternative_labels: z.array(z.string().min(1)).min(1).optional(), + // The citation system this work is cited under by default (ADR-0005). It + // governs the bare `/cite/{work}/{locator}` alias and default presentation + // only — it is identity-neutral and never affects how a fully qualified + // reference validates or resolves. The compiler checks that it names a + // known CitationSystem. + preferred_citation_system_key: FlatKey, creators: z.array(Creator).optional(), + // Compiler-derived projection of the work's MappingAssertions that are not + // deprecated, withdrawn or blocked (prov:alternateOf / dcterms:isReferencedBy + // in the published context, ADR-0006). The arrays enrich the work; they make + // no review claim. Read the MappingAssertion for status. Never authored. + alternateOf: z.array(Iri).optional(), + isReferencedBy: z.array(Iri).optional(), }); export const Work = WorkBase.superRefine((w, ctx) => { if (w.id !== `https://textrefs.org/id/work/${w.key}`) { ctx.addIssue({ - code: z.ZodIssueCode.custom, + code: 'custom', message: 'id MUST be https://textrefs.org/id/work/{key}', path: ['id'], }); } + // The rules the specification states for alternative_labels (§6). They are + // enforced here, on the published record, and not only on the authored + // YAML, so a record that reaches a consumer cannot carry a label twice. + // Uniqueness is deliberately local to one work: two different works MAY + // claim the same label (ADR-0007). + const seenLabels = new Set(); + (w.alternative_labels ?? []).forEach((label, i) => { + if (label === w.preferred_label) { + ctx.addIssue({ + code: 'custom', + message: `alternative label "${label}" repeats the preferred label`, + path: ['alternative_labels', i], + }); + } else if (seenLabels.has(label)) { + ctx.addIssue({ + code: 'custom', + message: `alternative label "${label}" is declared more than once for this work`, + path: ['alternative_labels', i], + }); + } + seenLabels.add(label); + }); }); export type Work = z.infer;