Skip to content

feat(api): add works and systems collection endpoints (#83) - #87

Merged
maehr merged 2 commits into
textrefs:stagingfrom
maehr:feat/83-collection-endpoints
Aug 24, 2026
Merged

feat(api): add works and systems collection endpoints (#83)#87
maehr merged 2 commits into
textrefs:stagingfrom
maehr:feat/83-collection-endpoints

Conversation

@maehr

@maehr maehr commented Aug 24, 2026

Copy link
Copy Markdown
Member

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 call loadWorks() and loadSystems().

This PR adds two static JSON-LD collections, and documents five bulk artifacts that already exist.

Path Content
/reg/works.json every Work, one @context, one @graph, sorted by key
/reg/systems.json every CitationSystem, same shape

Each item is the record that /id/…json serves, without a per-item @context. Records of every status appear — active, draft, and retired — and each keeps its status field, so a client can filter.

Files

  • src/lib/collection.ts (new) — JSONLD_CONTEXT, byKey, collectionBody. /reg/index.astro now imports byKey, 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 on src/pages/id/work/[key].json.ts.
  • src/pages/reg/index.astro — two link rel="alternate" elements.
  • api/openapi.yamlCollections and Bulk tags, both collection paths, the five /dump/ paths, and the WorkCollection / SystemCollection schemas.
  • src/content/docs/get-started/url-layout.md — a Collections and bulk data section, and an extended /reg/ table row.

The /dump/ artifacts

writeDump() in scripts/compile.ts already emits five files, and .github/workflows/pages.yml uploads all of dist. 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.json alternate. This PR adds systems.json as well, and gives both a title attribute. That matters more than expected: /reg/ already emits three hreflang alternates, so an untitled pair would be ambiguous to a naive client.

<link rel="alternate" hreflang="en" href="https://textrefs.org/reg/"/>
<link rel="alternate" hreflang="de" href="https://textrefs.org/de/reg/"/>
<link rel="alternate" hreflang="x-default" href="https://textrefs.org/reg/"/>
<link rel="alternate" type="application/ld+json" href="/reg/works.json" title="Works"/>
<link rel="alternate" type="application/ld+json" href="/reg/systems.json" title="Citation systems"/>

2. RegistryObject stays 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 at standard/schema/index.ts, so it is schema parity, not debris. Routing both endpoints through the union would also permit a MappingAssertion inside works.json, and a generated client would get a tagged union it must narrow before reading preferred_citation_system_key. WorkCollection and SystemCollection type each response exactly.

Open question

info.version stays at 0.1.0. These are additive paths, but a contract version bump belongs to the release process rather than to this PR. Say if you want 0.2.0 here instead.

Verification

npm run verify passes 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.json holds 12 works; dist/reg/systems.json holds 10 systems.
  • Every item parses against the canonical Zod Work / CitationSystem schemas (acceptance criterion 2).
  • Every collection is sorted by key and carries no per-item @context.
  • dist/reg/index.html emits both alternates; dist/dump/ still holds all five artifacts.

Acceptance criteria

  • 1 — both collections build and parse as JSON-LD
  • 2 — every item validates against Work / CitationSystem
  • 3 — /reg/ advertises the collections through link rel="alternate"
  • 4 — the contract documents both collections and all five /dump/ artifacts
  • 5 — url-layout.md describes the new paths
  • 6 — npm run verify passes

🤖 Generated with Claude Code

https://claude.ai/code/session_01MoyDrm1kdM6tuisCHarWk2

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
Copilot AI lite review requested due to automatic review settings August 24, 2026 18:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.json and /reg/systems.json.
  • Introduced a shared collectionBody/byKey helper (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.

Comment thread api/openapi.yaml
Comment on lines +173 to +175
content:
application/ld+json:
schema: { $ref: '#/components/schemas/WorkCollection' }

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 declared application/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
@maehr
maehr merged commit a4a3b85 into textrefs:staging Aug 24, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants