feat(api): add works and systems collection endpoints (#83) - #87
Conversation
Every operation in the v0.1.0 contract reads one record by its own identifier. A client must already know a key before it can fetch anything. Registry discovery was a build-time privilege, because only the `/reg/` browser could call `loadWorks()` and `loadSystems()`. Add two static JSON-LD collections at `/reg/works.json` and `/reg/systems.json`. Each body carries one `@context` and one `@graph` of records, sorted by key, in every status. Items are the records that `/id/…json` serves, without a per-item `@context`. Document the five `/dump/` artifacts that the compiler already writes and Pages already serves. The contract never mentioned them. - Add `src/lib/collection.ts`. It holds the shared sort order, so the HTML browser and the JSON collections cannot drift. - Advertise both collections from `/reg/` with `link rel="alternate"`. Each link carries a `title`, because the page also emits `hreflang` alternates. - Add `Collections` and `Bulk` tags, both paths, the five dump paths, and the `WorkCollection` / `SystemCollection` schemas. `RegistryObject` stays unreferenced. It mirrors the exported Zod union in `standard/schema/index.ts` and is schema parity, not debris. Closes textrefs#83 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoyDrm1kdM6tuisCHarWk2
There was a problem hiding this comment.
Pull request overview
Adds registry-discovery endpoints and documentation so HTTP-only clients can discover Works and CitationSystems without already knowing keys, aligning the static site’s build-time registry with a published contract.
Changes:
- Added static JSON-LD collection endpoints:
/reg/works.jsonand/reg/systems.json. - Introduced a shared
collectionBody/byKeyhelper (and tests) to ensure consistent key-sorted ordering across the HTML registry browser and JSON collections. - Documented the new collections and existing
/dump/bulk artifacts in OpenAPI and site docs.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/pages/reg/works.json.ts | New JSON-LD collection endpoint for all Works. |
| src/pages/reg/systems.json.ts | New JSON-LD collection endpoint for all CitationSystems. |
| src/pages/reg/index.astro | Uses shared byKey sort and advertises collection endpoints via link rel="alternate". |
| src/lib/collection.ts | Adds shared JSON-LD context constant and helpers for collection bodies + sorting. |
| src/lib/collection.test.ts | Tests context, sorting, immutability, and item pass-through for collections. |
| src/content/docs/get-started/url-layout.md | Documents the collections and existing /dump/ artifacts. |
| api/openapi.yaml | Adds Collections/Bulk tags, documents new /reg/*.json and /dump/* paths, and adds collection schemas. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| content: | ||
| application/ld+json: | ||
| schema: { $ref: '#/components/schemas/WorkCollection' } |
There was a problem hiding this comment.
Good catch on the inconsistency — fixed in 35def8e, but in the opposite direction to the one suggested.
The site is static. GitHub Pages derives every Content-Type from the file extension and discards the header the Astro APIRoute sets, so that header never reaches a client. The live site confirms it:
$ curl -sI https://textrefs.org/id/work/plato.republic.json | grep -i content-type
content-type: application/json; charset=utf-8
$ curl -sI https://textrefs.org/dump/works.jsonl | grep -i content-type
content-type: application/octet-stream
So /id/…json declaring application/json was already accurate to the wire. Switching those four paths to application/ld+json would have made the contract describe a response nobody receives. The inaccurate declarations were the two this branch introduced.
Changed instead:
- both
/reg/collections →application/json - the four
/dump/JSONL paths →application/octet-stream(they were declaredapplication/x-ndjson)
Every .json path in the contract is now application/json and every .jsonl path is application/octet-stream, which is both internally consistent and true.
Since the declared types no longer describe the payloads, info.description, the Bulk tag, and url-layout.md now state the rule explicitly: a .json body is JSON-LD by content, a .jsonl body is newline-delimited JSON, and a client should parse by documented shape rather than by response header.
One related thing left alone: the Content-Type: application/ld+json; charset=utf-8 header in the APIRoute handlers is effectively dead code under a static build. It would matter if the site ever moved to SSR, so it stays — out of scope for this PR.
Copilot found that the contract mixed `application/ld+json` on the new
collections with `application/json` on the record paths. The finding is
correct, but the direction is the opposite of the suggested one.
The site is static. GitHub Pages derives every `Content-Type` from the
file extension and drops the header that the Astro `APIRoute` sets. The
live site proves it:
/id/work/plato.republic.json -> application/json; charset=utf-8
/dump/works.jsonl -> application/octet-stream
So the record paths were already accurate, and the two media types this
branch added were not. Correct them instead.
- Declare `application/json` on both `/reg/` collections.
- Declare `application/octet-stream` on the four `/dump/` JSONL paths.
- Explain the rule in `info.description`, on the `Bulk` tag, and in
`url-layout.md`: parse by documented shape, not by response header.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MoyDrm1kdM6tuisCHarWk2
Closes #83
What this changes
Every operation in the v0.1.0 contract reads one record by its own identifier. A client must already know a key before it can fetch anything. Registry discovery was a build-time privilege, because only the
/reg/browser could callloadWorks()andloadSystems().This PR adds two static JSON-LD collections, and documents five bulk artifacts that already exist.
/reg/works.json@context, one@graph, sorted by key/reg/systems.jsonEach item is the record that
/id/…jsonserves, without a per-item@context. Records of every status appear — active, draft, and retired — and each keeps itsstatusfield, so a client can filter.Files
src/lib/collection.ts(new) —JSONLD_CONTEXT,byKey,collectionBody./reg/index.astronow importsbyKey, so the HTML browser and the JSON collections cannot drift in order.src/lib/collection.test.ts(new) — four cases: context value, sortedness, no input mutation, item pass-through.src/pages/reg/works.json.ts,src/pages/reg/systems.json.ts(new) — modelled onsrc/pages/id/work/[key].json.ts.src/pages/reg/index.astro— twolink rel="alternate"elements.api/openapi.yaml—CollectionsandBulktags, both collection paths, the five/dump/paths, and theWorkCollection/SystemCollectionschemas.src/content/docs/get-started/url-layout.md— a Collections and bulk data section, and an extended/reg/table row.The
/dump/artifactswriteDump()inscripts/compile.tsalready emits five files, and.github/workflows/pages.ymluploads all ofdist. The five files are therefore live today, and the contract never mentioned them. This PR documents them. No build behaviour changes.Two decisions worth review
1. Criterion 3 is extended. The issue asks only for a
works.jsonalternate. This PR addssystems.jsonas well, and gives both atitleattribute. That matters more than expected:/reg/already emits threehreflangalternates, so an untitled pair would be ambiguous to a naive client.2.
RegistryObjectstays unreferenced. The issue reads it as a dangling schema and a ready-made item type. This PR does not use it. It mirrors the exported Zod union atstandard/schema/index.ts, so it is schema parity, not debris. Routing both endpoints through the union would also permit aMappingAssertioninsideworks.json, and a generated client would get a tagged union it must narrow before readingpreferred_citation_system_key.WorkCollectionandSystemCollectiontype each response exactly.Open question
info.versionstays at0.1.0. These are additive paths, but a contract version bump belongs to the release process rather than to this PR. Say if you want0.2.0here instead.Verification
npm run verifypasses in full against the real registry — 204,359 pages, 67,959 references, all internal links valid.Checked against the real registry, not only the fixture:
dist/reg/works.jsonholds 12 works;dist/reg/systems.jsonholds 10 systems.Work/CitationSystemschemas (acceptance criterion 2).@context.dist/reg/index.htmlemits both alternates;dist/dump/still holds all five artifacts.Acceptance criteria
Work/CitationSystem/reg/advertises the collections throughlink rel="alternate"/dump/artifactsurl-layout.mddescribes the new pathsnpm run verifypasses🤖 Generated with Claude Code
https://claude.ai/code/session_01MoyDrm1kdM6tuisCHarWk2