Designing a system that will not state what it cannot prove.
A generator writes job application documents, traces every factual claim to the record behind it, and blocks the ones that have none. The subject matter is a job search; the problem is not. Anything that produces text someone will act on — a summary, a report, an answer pulled from a pile of documents — has to know which parts it can back and which it cannot.
What that looks like in practice, and what this repository is actually demonstrating:
- Provenance is the schema, not a feature. A claim carries its label and its source together. The kind of record decides which label is available: a self-report can only support "the subject said it", never "a record confirms it".
- Refusal is the main path. A claim with no record is blocked before it reaches a document. The pipeline reports how many records it stopped, not how many it sent — 11 of 14 here.
- The invariants are executable.
npm run seedthrows rather than emit a claim whose label and record disagree, andnpm run checkfails the build if one ever appears. None of this relies on remembering the rules. - Refusal survives export. The PDF a reader downloads contains no blocked claim text, and a test generates the file and asserts that.
The page makes it visible: the record library on the left, the document in the middle, the job requirement on the right, and a line connecting each claim to both.
The demo data is generated. No real record enters this repository. Every company, person,
place and file name is invented. The page belongs to Russell W. Hild / consultruss.com, and
deploys to consultruss.com/provenance.
npm run seed # regenerate data/generated/* and src/js/data.js from data/seed.config.json
npm run build # write dist/
npm run check # run the acceptance checks
npm run serve # preview dist/ at http://127.0.0.1:8137No dependencies. Node is used for the four scripts only; the page itself is plain HTML, CSS and ES modules.
Preview with npm run serve rather than opening index.html from the filesystem: module
scripts are CORS-checked and a file:// origin is opaque, so browsers refuse to load them.
The server sends Cache-Control: no-store, because a browser will otherwise keep an old
js/data.js after npm run seed and quietly show you stale numbers. If you serve dist/
some other way, send that header or you will debug a page that is not the one you built.
| Label | Meaning | Ships |
|---|---|---|
| a | said directly by the subject | yes |
| b | confirmed against a record | yes |
| c | asserted by a tool, never confirmed | no |
| d | exists only in a derived document | no |
A claim labelled c or d carries sourceId: "s0", the null source. There is nothing to cite, so
it draws no wire on the left. npm run check fails if that is ever untrue.
data/seed.config.json every invented name and every authored string
data/generated/*.json generator output, committed, meant to be opened and read
scripts/generate.mjs writes the four JSON files and src/js/data.js
scripts/build.mjs copies src/ to dist/
scripts/check.mjs the acceptance checks
src/ the page
design/provenance-mock.html the visual reference this was built against, not shipped
dist/ build output, committed, deployed
Three files sit outside the original brief and are worth explaining:
src/js/data.jsis generated alongside the JSON. The page imports it instead of fetching it, so the page issues no data request at all and needs no base-path logic to work from a subdirectory. The JSON files stay the artifact a reviewer opens; this is the same content as a module.src/js/pdf.jswrites PDFs directly, with no library. The documents are short and text-only, and the base-14 fonts need no embedding, so a few hundred lines replaces a ~350 KB dependency and keeps the no-CDN rule literally true.src/js/dom.jsis a fifteen-linecreateElementhelper. Nothing on the page is built from interpolated HTML strings.
The download button writes a PDF of the document on screen, and the export performs the refusal rather than working around it:
- A blocked claim is never written to the file. It is replaced in place by
[ claim withheld — no source on file ]. - Every claim that ships carries a footnote naming the source file it came from, so the document keeps its provenance once detached from the page.
- The creation date is pinned to
seed.config.json, so the same document always produces a byte-identical PDF.
This was added after the brief was written; the brief listed PDF export as out of scope, and that decision was reversed deliberately.
npm run seed is a pure function of data/seed.config.json. There is no Date.now(), no
Math.random(), and no Intl/locale formatting anywhere in the generator — locale output
depends on the ICU build Node was compiled with, which would make the seed machine-dependent.
The generator asserts its own totals and throws rather than emitting a corpus that does not add
up.
npm run check decides these automatically:
| Check | What it proves |
|---|---|
| 16.1 | npm run seed twice is byte-identical |
| 16.2 | no real company name, no name belonging to another project on this machine |
| 16.3 | every claim labelled c or d cites s0, and no shipping claim cites it |
| 16.6 | the first document carries a blocked claim to open on |
| 16.8 | the gate data contains at least one exit 1 run |
| 16.9 | at least one requirement lands on do not have |
| 16.11 | nothing in src/ or dist/ references an external host |
| 16.12 | the exported PDF draws no blocked claim text, and footnotes every claim that ships |
The denylist in check 16.2 is a list, not a guess. Extend it in scripts/check.mjs rather than
relying on memory.
These four are checked by hand in a browser, because a script cannot decide them:
- 16.4 — click a claim, then a source, then a requirement, then an inspector chip to return to a claim. The console stays clean at every step.
- 16.5 — switching documents leaves both rails unchanged and updates every requirement dot.
- 16.7 — resizing the window redraws the wires onto the correct anchors.
- 16.10 — keyboard alone reaches and activates every claim, source and requirement.
npm run build writes dist/. Every asset path is relative, so the folder works from any
subdirectory. Deployment copies dist/ into the site's deploy folder under /provenance.
Nothing in this repository touches the site repository.