feat: serve the flashtrace JSON Schemas verbatim#9
Merged
Conversation
Copies the tool repo's schemas/ into dist/ byte-for-byte and adds a generated latest.<format> alias per schema directory. The files never touch the markdown pipeline: consumers fetch them by $id, and the docs link rewriter would corrupt the identifiers ($id, $ref, $schema) inside them. latest.json is a real file rather than a redirect - GitHub Pages has no server-side redirects and a meta-refresh means nothing to a JSON fetch. Its $id is not rewritten, so a copy fetched from latest.json still identifies the version it actually is. schemas/ is absent until the release that introduces it, so a missing folder only warns; CI and deploys build from the latest published release and must keep working. Once the folder exists, anything wrong inside it is fatal. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jul 20, 2026
Discovery walked schemas/ to any depth, accepted any extension, and the build then cpSync'd the whole folder into dist/. That published whatever upstream happened to put there - a README meant for repo readers, a symlink, a format nobody had decided to support - and left the site serving paths it had never validated. Discovery now recognises exactly schemas/<name>/v<N>.<format>, one directory deep, with an explicit format allowlist that holds only json today. The build writes the files discovery accepted instead of copying the folder, so the site publishes the layout it checked and nothing else. Everything found and not served is recorded as a problem, distinguishing files that are plainly local to the tool repo (README, dotfiles, which stay silent) from names that were reaching for the version layout and missed (v1.2.json, report-v0.json, a symlink where a schema belongs). A typo that unpublishes a schema is otherwise indistinguishable from a file that was never meant to be one. Problems remain fatal for now; the next commit makes them survivable. An empty schemas/ no longer fails - the folder can land upstream a release before the first schema inside it does. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A single unservable file under schemas/ aborted the build. Since both CI and deploy build from the newest flashtrace release, that meant one bad schema in a release stopped the site from deploying at all - docs included - and left it stale until upstream cut a fix. The blast radius was wrong. Refusing to publish a broken schema is right; refusing to publish the docs because a schema is broken is not. Skipped files are now warnings, the build finishes, and everything that validated still ships. Quiet skipping would be its own failure, so the build writes the skips to schema-problems.json (gitignored, outside dist/, not published). Written on every build including a clean one, so the workflow reading it can tell "nothing wrong" from "never got that far". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A readFileSync failure inside discovery surfaced as a bare `error: EACCES ...` with nothing tying it to schemas/, leaving the reader to work out which file it meant. It is now reported like every other skip, naming the file, and the build carries on - an unreadable schema is indistinguishable from a broken one from a visitor's side, so it deserves the same treatment. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every version entry carried the parsed document alongside its bytes, and nothing in this PR read it - the site serves bytes and never a re-serialisation. The parse itself stays: it is what catches a release shipping a schema that will not parse, and that check is the reason the URL can be treated as a contract. Only the result is discarded, so there is no second representation of a schema able to drift from the one being served. Note for #10, which is stacked on this branch: src/schema-doc.mjs reads versions[].json to render its page. It should parse the bytes where it needs the document, keeping the parsed form local to the consumer that wants it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
collectSchemaDir decided what every entry was and accumulated the result in one loop, reaching a cognitive complexity of 25 against a limit of 15. The two jobs are separable: classifying an entry needs nothing but the entry, and grouping needs nothing but the classification. classifyEntry now answers with exactly one of a version to serve, a problem to report, or null for a file that is the tool repo's own business, which leaves the walk short enough to read at a glance. No behaviour change - the synthetic checkout reports the same six problems and serves the same five files. Also drops the redundant A-Z half of the case-insensitive latest.<format> character class. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Skipping a broken schema keeps the site deploying, but a skip nobody sees is just a silent outage of that schema - and a green deploy is exactly where nobody looks. CI log output alone does not carry: the release that breaks a schema is usually not the one anyone is watching. Every run annotates each skipped file. Deploys additionally maintain one tracking issue: opened on the first bad build, commented when the problem set changes, silent when it has not, and closed automatically on the first clean build. Deduplicated by a fingerprint of the problem set carried in an HTML comment, so a persistent problem across ten deploys is one issue with one comment rather than ten issues. PR builds annotate but never file - a PR should not open an issue about upstream content it did not touch. The step is continue-on-error: reporting a problem must never become a worse problem than the one being reported, which is the whole premise of not failing the build in the first place. report() takes gh as a parameter and the entry point sits behind an argv guard, so the decision logic can be driven end to end against a fake with no path to the real CLI. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
public/ is copied over dist/ last, so a file there wins against a generated file at the same path without a word. That is deliberate - it is what makes public/ an escape hatch - but it is invisible at the point where it would bite: a public/schemas/ or public/docs/ would shadow the real thing and the build would still look right. Noted at the copy in build.mjs, where someone debugging a wrong file will be looking, and in the CLAUDE.md structure table, where someone deciding to put a file in public/ will be. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jul 20, 2026
The issue reported problems by accumulating comments, so the current state was whatever the newest comment said and a reader had to reconstruct it by scrolling. Someone opening the issue should see what is wrong now. The body is now the live view: a short instruction and a table of the most recent build's skips, rewritten in place on every change without touching the issue's state. Each change also appends a comment saying what actually moved - which files stopped being served, which came back, which are failing for a different reason than before - so the history is still there for anyone who wants it. The body carries the problem set it was written from, in a comment marker. That is what lets the next run tell an unchanged set from a changed one and diff the two, with no state kept anywhere else to fall out of step with the issue. A body edited into nonsense degrades to "cannot diff" and is rewritten, rather than producing a wrong diff. Closing now rewrites the body first, so someone arriving from the close notification does not land on a table of problems that no longer exist, and the closing comment lists what was fixed. Reasons are a parser's own words, so table cells escape pipes and newlines and the embedded state escapes > - an error message quoting JSON should not be able to break the row or close the comment early. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The schema reports named only the flashtrace release, but that is the one coordinate a fix here does not move. A layout that changed upstream on purpose is resolved by teaching src/schemas.mjs about it and merging to main, which redeploys against the same latest release - so the issue read "problems at v1.2.3" then "resolved at v1.2.3", crediting a release that never changed and leaving no way to see why it works now. The open direction was equally wrong: a regression in src/schemas.mjs filed an issue blaming an innocent release. Every body and comment now states both: the release the docs and schemas were built from, and the linked .github.io commit that built them. Taken from GITHUB_SHA, falling back to the local checkout, and omitted entirely when neither is available. Deliberately not part of the fingerprint - the sha moves on every merge to main, so folding it in would make unrelated deploys re-comment on an unchanged issue. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A reason is a parser's own words, so it can hold a newline - and a newline in a workflow command ends that command and starts whatever follows it, letting upstream content emit ::add-mask::, ::error:: or any other command into the run's stream. cell() already collapses newlines for the table path; the annotation path did not use it. Reuse it there, and add GitHub's own percent escaping on top: %25/%0D/%0A for the message, plus %3A/%2C for title=, where : and , separate the properties. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A null previous state means "cannot diff", not "diffed to nothing", but it was passed as `previous ?? []` into diffProblems - so an issue body that had lost its state marker produced a comment announcing every long-standing problem as newly broken, and a close comment with a silent gap where the list of recovered files should be. Honour the distinction the comment already claimed: an unreadable state gets its own comment stating the current set and saying plainly that it is not a comparison, and closeComment says so rather than listing nothing. Both self-heal - the body is re-stamped in the same run, so the next build can diff again. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The close path writes the body before closing, so a reader arriving from the close notification does not land on a table of problems that no longer exist. Nothing caught a failure between the two calls, though, and execFileSync throws on a non-zero gh: a failed close left an open issue whose body claims to be resolved and whose table is gone - worse than either call not having happened. Keep the ordering, and make it recoverable. The pre-existing body is now retained rather than only parsed, and a failed close puts it back, so the next clean build retries the whole thing. If the restore fails too, the log says exactly what state the issue is in. The error is rethrown either way: the step is continue-on-error, so it surfaces on the run without failing the deploy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
JSON.stringify over the raw array folds array order and key insertion order into the hash, so a discovery pass returning the same problems in a different order - or upstream adding an incidental field like a line number or a timestamp - would read as a changed set and re-comment on every deploy. The comment claimed discovery sorts by path; nothing here enforced it. Normalise to sorted [path, reason] tuples before hashing. Sorted by code point rather than localeCompare, which can call two distinct paths equal and leave their order dependent on arrival order - the same instability. No migration: the fingerprint stored in the issue body is informational, and the staying-quiet check recomputes both sides from their problem sets, so an issue open across this change still compares correctly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Without --repo, gh infers the repository from the working directory, so the script depended on being run from a checkout whose origin points at this repository - an assumption the sha handling already declines to make. Pass $GITHUB_REPOSITORY when it is set, and keep gh's inference for local runs without it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Keeps the rationale that is not derivable from the source - why schemas bypass the markdown pipeline, why latest is a file rather than a redirect, why the close is ordered body-then-state - and drops the prose that narrated what the next line already says. Also removes three points that were made twice: the parsed JSON being discarded (header and parse site), the public/ shadowing rule (build.mjs and CLAUDE.md), and the cannot-diff distinction (state read and its consumers). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
Publishes the machine-readable JSON Schemas the tool repo ships under
schemas/, built from the same release checkout/docs/already comes from — so the site cannot disagree with itself about which version it serves.This is the serving half of #8, split out so it can merge quickly. The generated documentation page per schema follows in a separate PR.
What lands
/schemas/report/v0.jsonschemas/report/v0.json/schemas/report/latest.jsonNo page, no nav entry, no styling — nothing user-visible changes.
Decisions worth reviewing
The schemas never touch the markdown pipeline. Written byte-for-byte and served as-is. The strings inside them (
$id,$ref,$schema) are identifiers, not navigation, and the docs link rewriter would corrupt them.latest.jsonis a real file, and its$idis not rewritten. It keeps"$id": ".../schemas/report/v0.json". JSON Schema allows the retrieval URI to differ from$id, which is the point: a copy fetched fromlatest.jsonmust still identify the version it actually is, or every vendored copy would claim to be current. A redirect was not an option — GitHub Pages has no server-side redirects, and a meta-refresh means nothing to a JSON fetch.Only
schemas/<name>/v<N>.jsonis served. Exactly one directory deep, with an explicit format allowlist holding justjsontoday. The build writes the files it validated rather than copying the folder, so nothing reaches the site that was not checked — not a stray file, not a symlink, not a format nobody decided to support. Addingxmllater is a one-word change.A broken schema does not stop the deploy. Both CI and deploy build from the newest flashtrace release, so making one bad file fatal meant one bad release could stop the site deploying at all — docs included — and leave it stale until upstream cut a fix. That blast radius was wrong: refusing to publish a broken schema is right, refusing to publish the docs because a schema is broken is not. Unservable files are skipped individually and everything that validated still ships.
Skips are surfaced as a tracking issue, not just a log line. A skip nobody sees is a silent outage of that schema, and a green deploy is exactly where nobody looks. Every run annotates each skipped file; deploys additionally maintain one issue. PR builds annotate but never file, since a PR should not open an issue about upstream content it did not touch. The step is
continue-on-error— reporting a problem must never become a worse problem than the one reported.That issue is a live view, not a log. Its body always shows the most recent build's skips, rewritten in place; each change also appends a comment saying what moved — which files stopped being served, which came back, which now fail for a different reason. Opening the issue tells you what is wrong now without scrolling; the comments keep the history for anyone who wants it. The body carries the problem set it was written from in a comment marker, which is what lets the next run tell an unchanged set from a changed one and diff the two — no state stored anywhere else to fall out of step. It opens on the first bad build, stays completely silent while nothing changes, and closes itself on the first clean one, rewriting the body first so nobody arrives from the close notification onto a stale table.
Files that are plainly the tool repo's own stay silent. A
README.mdexplaining the folder to someone reading that repo, dotfiles, an empty placeholder folder: not served, not reported. What is reported is a name that was reaching for the layout and missed —v1.2.json,report-v0.json, a symlink where a schema belongs — because a typo that unpublishes a schema is otherwise indistinguishable from a file that was never meant to be one.Version files are re-copied every build.
schemaVersiononly moves on breaking changes, sov0.jsonis not write-once — it keeps receiving additive updates within its version.A missing
schemas/only warns. It is absent until the release that introduces it, and CI builds against the latest published release. An emptyschemas/warns too: the folder can land upstream a release before the first schema inside it does.Verified locally
Built against a checkout that has
schemas/(thefeat-json-reportupstream worktree), one that does not, and a synthetic one carrying every problem class at once./schemas/report/v0.jsonis byte-identical to the upstream file (cmp)/schemas/report/latest.jsonis byte-identical tov0.json(cmp)$idin both readshttps://flashtrace.github.io/schemas/report/v0.jsonschemas/: warns, skips, builds 13 pages as before, nodist/schemas/.xmlversion,v1.2.json, an upstreamlatest.json, a file outside a name folder, and a directory nested too deep are each reported and skipped — the build exits 0 and still serves the five good filesv10.jsonleaveslatest.jsononv9.jsonrather than on nothing, and the skip says whyv10sorts abovev9;README.mdand an empty folder are silently ignoredgh: annotate-only never callsghat all; open / update-in-place / stay-silent / close each fire on the right state; the body is re-stamped with the new set and the comment names exactly what moved; closing rewrites the body before changing state; a body whose state marker was deleted degrades to "cannot diff" rather than to a wrong diff; and a reason containing a pipe, a newline or-->breaks neither the table nor the embedded stateStill needs checking after the first deploy
Two response-header requirements cannot be verified before the site is live, and neither is controllable from this repo — GitHub Pages has no
_headerssupport:Expecting
200,Content-Type: application/json, andAccess-Control-Allow-Origin: *. Pages is expected to do both by default, but that is worth confirming rather than assuming — third-party validators fetching the schema by$iddepend on the CORS header. If either is wrong we need a different hosting path for/schemas/.Sequencing
schemas/is not in a flashtrace release yet, so this merges safely now and stays dormant: the build warns and skips. The schemas appear on the site only after a flashtrace release containing them — a merge to the tool repo's main is not enough, since both CI and deploy build from a release tag.#10 is stacked on this branch and reads
versions[].json, which this PR removes; it needs to parse the bytes where it uses the document.🤖 Generated with Claude Code