Skip to content

refactor(compile): remove duplication and detect duplicate registry keys - #153

Merged
maehr merged 2 commits into
textrefs:stagingfrom
maehr:refactor/compile-structure
Sep 7, 2026
Merged

maehr merged 2 commits into
textrefs:stagingfrom
maehr:refactor/compile-structure

Conversation

@maehr

@maehr maehr commented Sep 7, 2026 •

Copy link
Copy Markdown
Member

What this does

This pull request removes repeated code from the registry compiler. It adds three integrity guards. It changes no compiled output.

The work follows the review of PR #4. scripts/compile.ts mints every identifier in the release, so it is the file that most needs to be clear before the v0.1.0 tag.

Changes

One validation helper. Four record types repeated the same safeParse, report and throw block. parseRecord replaces all four. scripts/validate-data.ts prints issues with the same helper. It keeps its own control flow, because it counts a failure and continues.

Duplicate key detection. A second citation system file overwrote the first in the Map. A second work file had no check at all, and it produced two Work records under one @id with conflicting metadata.

Nothing downstream objected to that. scripts/validate-data.ts validates each record on its own and never checks that an @id is unique, so it reported the registry as valid. release.yml runs npm run build:data, which compiles and validates but never builds the site. A duplicate therefore reached the release artifact and the Zenodo record unchallenged. The compiler now fails and names both files.

One case was already caught: two work files that declare different citation systems for one locator collide on the bare /cite/{work}/{locator} alias, and setAlias rejects that. The guard covers the cases that were silent.

Frozen identifiers. scripts/validate-data.ts recomputes every UUID from the ADR-0002 seed, independently of the compiler. That proves the two implementations agree. It cannot prove either one still mints what the registry published, because both are code: change the seed rule in the compiler and in the validator, which is the natural move when the check goes red, and every assertion passes while all 86,397 reference identifiers move.

Fifteen identifiers are now frozen as data — one reference per citation system, and one mapping per relation, since the relation is part of the mapping seed. No edit to any implementation can satisfy a literal. Verified by changing the seed rule in both implementations: the recomputation check passed, and only the frozen values caught it.

A warning for every skipped resolver. The block resolvers and the per-reference extra_resolvers run as two loops. Only the first loop counted a skipped entry. A hole in a per-reference map stayed silent, which is the one outcome applyResolverVars exists to prevent.

A typed resolver target. The compiler builds the entry as Pick<ResolverTargetEntry, 'url'> & Partial<ResolverTargetEntry>. TypeScript now checks the field names and the values. A typo fails the build instead of reaching safeParse.

One definition for the dump. DUMP_MANIFEST and dumpResources each listed the same five resources. Both now derive from one DUMP_SPECS list, so they cannot drift. Each body stays a thunk, so /dump/index.astro still renders the file list without serialising ~90 MB.

A shared IRI module. standard/iri.ts holds the four IRI prefixes. It is dependency-free, so src/lib/find.ts can import it and still ship to the browser.

Two extractions. emitMappings and systemBlocksOf come out of compileRegistry. enforceRegistryInvariants returns void; it always returned 0.

What this deliberately does not change

The UUID seeding in scripts/validate-data.ts stays duplicated. Importing the compiler's own function would make every assertion a tautology. The duplication is a real tripwire against a one-sided change, and the frozen identifiers above cover the two-sided change it cannot catch.

The larger idea of a declarative source YAML to emitter pipeline is out of scope. The intermediate model already exists in two layers: scripts/source-schema.ts holds the authored shape, and standard/schema/ holds the published shape. A third layer adds a step without removing one. It would also dissolve the ADR commentary, which is the most valuable content in the file.

Verification

The dump is byte-identical. All five resource bodies keep the same sha256 as staging. The data package descriptor matches, apart from its created timestamp.

references.jsonl        303b72d0a81090a922e199c3a5c35c0d2e06cdcdfa313553958cc77a31d478b7
mappings.jsonl          4e9054f23a3c3abcbbf2b47a3a7f023d61a25e0ff80a746f4b4102d9b353c746
works.jsonl             5c29426650959906345b30a72491a1cf9a958f53ea06b206ead24c20d586ab7e
aliases.json            782b8de80a36e13d463a6d33df558b086c0a1c9875c655e2ba0c2f8affe5b805
citation-systems.jsonl  7e95ef717fab0701d0cc3731453bb8fc6a12409f22f833188323b0b254a7983c

Compile time does not change. Interleaved runs of compileRegistry(), median of seven:

Round staging this branch
1 756 ms 755 ms
2 746 ms 754 ms
3 761 ms 753 ms

An earlier revision of this branch built the resolver target from conditional spreads. That allocated a throwaway object per field, three times slower over 172k targets, so it was replaced by the assignments described above.

Gates. npm run verify passes: format, astro check with 0 errors, 179 tests, 259,815 pages built, internal links valid. npm run validate:data reports 86,492 of 86,492 records valid and 15 frozen identifiers unchanged.

New tests. Two tests cover the duplicate key guards. One test replaces the DUMP_MANIFEST drift test, which guarded a duplication that no longer exists.

Notes for the reviewer

This is build tooling, which AGENTS.md routes through main. It targets staging because it must reach the v0.1.0 release in PR #4.

Merging this moves the staging tip. PR #4 then needs a github-pages dispatch on the new tip. Two release items are still open and are not in this pull request, because a squash merge would make them stale again: npm run changelog needs a re-run, and CITATION.cff still carries date-released: '2026-08-12'.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VLqvaFc9mqQYcjb9SiKGXd

Copilot AI lite review requested due to automatic review settings September 7, 2026 16:42

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.

🔵 Needs a closer look

It refactors and extends the identifier-minting registry compiler, so a final human review is warranted despite added tests and stated byte-identical outputs.

Pull request overview

This PR refactors the registry compiler (scripts/compile.ts) to remove repeated validation/output construction patterns, and adds integrity guards (duplicate key detection; resolver skip warnings) while keeping the compiled dump output unchanged. It also centralizes TextRefs IRI construction in a small, dependency-free module so both Node tooling and browser-shipped code can share canonical IRI spelling.

Changes:

  • Introduces standard/iri.ts and updates call sites to use shared workIri/systemIri/refIri/mappingIri helpers.
  • Refactors compiler record validation/error reporting via parseRecord + printIssues, and tightens resolver target typing.
  • Adds compiler invariants: fail fast on duplicate registry keys (works, systems) and warn consistently on skipped resolvers; updates tests accordingly.
File summaries
File Description
standard/iri.ts New dependency-free module defining canonical TextRefs IRI prefixes/builders.
src/lib/find.ts Uses shared refIri() for reference IRI construction in browser-shipped finder logic.
scripts/validate-data.ts Reuses compiler’s issue-printing helper and shared IRI builders while keeping UUID derivation independent.
scripts/compile.ts Core refactor: shared safeParse reporting, duplicate key guards, resolver skip warnings, dump spec deduplication, typed resolver targets.
scripts/compile.test.ts Adds tests for duplicate key guards and updates dump-manifest coverage to match new DUMP_SPECS structure.
Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread scripts/compile.ts
@maehr
maehr force-pushed the refactor/compile-structure branch from 6de17d6 to 181ec39 Compare September 7, 2026 17:05
maehr and others added 2 commits September 7, 2026 19:26
Reduce the repeated code in the registry compiler. Add two integrity guards.
The compiled output does not change: all five dump resources keep the same
sha256, and the data package descriptor is identical. Compile time does not
change either: the median of seven runs is 754ms before and after.

Share one validation helper. Four record types repeated the same safeParse,
report and throw block. `scripts/validate-data.ts` now prints issues with the
same helper, but keeps its own control flow: it counts a failure and continues.

Detect a duplicate work key and a duplicate citation system key. A second
system file overwrote the first in the Map. A second work file had no check at
all, and it produced two Work records under one `@id` with conflicting
metadata. `scripts/validate-data.ts` accepted that, because it validates each
record on its own and never checks that an `@id` is unique. `release.yml` runs
`npm run build:data`, which compiles and validates but never builds the site,
so the duplicate reached the release artifact and the Zenodo record with
nothing to object. The compiler now fails and names both files.

One case was already caught. Two work files that declare different citation
systems for one locator collide on the bare `/cite/{work}/{locator}` alias, and
`setAlias` rejects that. The guard covers the other cases, which were silent.

Count a skipped resolver from `extra_resolvers`. The block resolvers and the
per-reference extras run as two loops, and only the first loop warned. A hole
in a per-reference map stayed silent. The loops stay separate: one loop over a
concatenation allocates an array per reference, 86k of them, to save four
lines.

Type a resolver target while it is built. TypeScript now checks the field names
and the values. The fields stay separate assignments, because a literal of
conditional spreads allocates a throwaway object per field and runs three times
slower over 172k targets. The published key order does not change, so the dump
bytes do not change.

Declare the five dump resources once, in `DUMP_SPECS`. `DUMP_MANIFEST` is
derived from it, so the two cannot drift. Each body stays a thunk, so
`/dump/index.astro` still renders the file list without serialising ~90 MB.

Add `standard/iri.ts` for the four IRI prefixes. It is dependency-free, so
`src/lib/find.ts` can import it and still ship to the browser.

Keep the UUID seeding in `scripts/validate-data.ts` duplicated on purpose. The
gate proves the compiler's identifiers are deterministic. A shared function
would make each assertion a tautology.

Extract `emitMappings` and `systemBlocksOf`. Make `enforceRegistryInvariants`
return void; it always returned 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLqvaFc9mqQYcjb9SiKGXd
…e-minting

`scripts/validate-data.ts` recomputes every reference and mapping UUID from the
ADR-0002 seed, independently of the compiler. That check proves the two
implementations agree. It cannot prove either one still mints the identifiers
the registry published, because both are code.

Change the seed rule in `scripts/compile.ts` and in this file, which is the
natural move when the check goes red, and every assertion passes while all
86,397 reference identifiers move. Nothing else in the repository records what
those identifiers are.

Freeze fifteen of them as data: one reference per citation system, and one
mapping per relation, because the relation is part of the mapping seed. No edit
to any implementation can satisfy a literal. The values come from the v0.1.0
baseline that PR textrefs#4 tags.

The check runs where the registry is already compiled, so it costs nothing.
`release.yml` runs `npm run build:data`, so it guards the exact path that
produces the release artifact and the Zenodo record.

A failure here is not a test to fix. It reports that a published citation has
stopped resolving.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLqvaFc9mqQYcjb9SiKGXd
@maehr
maehr force-pushed the refactor/compile-structure branch from 181ec39 to 0df1848 Compare September 7, 2026 17:27
@maehr
maehr merged commit 8223920 into textrefs:staging Sep 7, 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