refactor(compile): remove duplication and detect duplicate registry keys - #153
Merged
Merged
Conversation
There was a problem hiding this comment.
🔵 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.tsand updates call sites to use sharedworkIri/systemIri/refIri/mappingIrihelpers. - 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.
maehr
force-pushed
the
refactor/compile-structure
branch
from
September 7, 2026 17:05
6de17d6 to
181ec39
Compare
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
force-pushed
the
refactor/compile-structure
branch
from
September 7, 2026 17:27
181ec39 to
0df1848
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.tsmints 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.parseRecordreplaces all four.scripts/validate-data.tsprints 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 twoWorkrecords under one@idwith conflicting metadata.Nothing downstream objected to that.
scripts/validate-data.tsvalidates each record on its own and never checks that an@idis unique, so it reported the registry as valid.release.ymlrunsnpm 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, andsetAliasrejects that. The guard covers the cases that were silent.Frozen identifiers.
scripts/validate-data.tsrecomputes 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_resolversrun as two loops. Only the first loop counted a skipped entry. A hole in a per-reference map stayed silent, which is the one outcomeapplyResolverVarsexists 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 reachingsafeParse.One definition for the dump.
DUMP_MANIFESTanddumpResourceseach listed the same five resources. Both now derive from oneDUMP_SPECSlist, so they cannot drift. Each body stays a thunk, so/dump/index.astrostill renders the file list without serialising ~90 MB.A shared IRI module.
standard/iri.tsholds the four IRI prefixes. It is dependency-free, sosrc/lib/find.tscan import it and still ship to the browser.Two extractions.
emitMappingsandsystemBlocksOfcome out ofcompileRegistry.enforceRegistryInvariantsreturns void; it always returned0.What this deliberately does not change
The UUID seeding in
scripts/validate-data.tsstays 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.tsholds the authored shape, andstandard/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 itscreatedtimestamp.Compile time does not change. Interleaved runs of
compileRegistry(), median of seven: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 verifypasses: format,astro checkwith 0 errors, 179 tests, 259,815 pages built, internal links valid.npm run validate:datareports 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_MANIFESTdrift test, which guarded a duplication that no longer exists.Notes for the reviewer
This is build tooling, which
AGENTS.mdroutes throughmain. It targetsstagingbecause it must reach the v0.1.0 release in PR #4.Merging this moves the
stagingtip. PR #4 then needs agithub-pagesdispatch 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 changelogneeds a re-run, andCITATION.cffstill carriesdate-released: '2026-08-12'.🤖 Generated with Claude Code
https://claude.ai/code/session_01VLqvaFc9mqQYcjb9SiKGXd