Skip to content

feat: serve and document flashtrace JSON Schemas#8

Closed
MentorFilou wants to merge 5 commits into
mainfrom
feat/schema-publishing
Closed

feat: serve and document flashtrace JSON Schemas#8
MentorFilou wants to merge 5 commits into
mainfrom
feat/schema-publishing

Conversation

@MentorFilou

Copy link
Copy Markdown
Member

Serves the machine-readable JSON Schemas the tool repo publishes under schemas/, and generates a human-readable page per schema from those files. Both are built from the same release checkout /docs/ already comes from, so they cannot disagree about which version the site describes.

What lands

URL What
/schemas/report/v1.json verbatim copy of the release's schemas/report/v1.json
/schemas/report/latest.json byte copy of the highest version
/docs/schemas/report/ generated reference, one #vN section per version

Decisions worth reviewing

The schemas never touch the markdown pipeline. They are copied with cpSync and served as-is. The strings inside them ($id, $ref, $schema) are identifiers, not navigation, and the docs link rewriter would corrupt them.

latest.json is a real file, and its $id is not rewritten. It keeps "$id": ".../schemas/report/v1.json". JSON Schema allows the retrieval URI to differ from $id, which is the point: a copy fetched from latest.json must 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.

The renderer fails loudly. An unrecognized keyword aborts the build naming its JSON pointer (#/$defs/item/properties/tags/maxItems) rather than rendering a page that silently drops a constraint. Extending KNOWN_KEYWORDS in src/schema-doc.mjs means deciding how the new constraint displays — it should not be widened just to get a build green.

Conditionals are resolved, not dropped. The top-level allOf is walked alongside the base schema by instance location, so then.required: ["forwards"] annotates the top-level table and then.properties.items.items.required: ["wantedBy"] resolves through the element $ref and annotates the item definition. Both render as Required when mode is "rich" instead of a misleading Optional.

Version files are re-copied every build. schemaVersion only moves on breaking changes, so v1.json is not write-once — it keeps receiving additive updates within version 1. Nothing skips a version file for already existing.

A missing schemas/ only warns. It is absent until the release that introduces it, and CI builds against the latest published release — so hard-failing would break CI and deploys today for a folder that upstream has not shipped yet. Once the folder exists, anything wrong inside it is fatal.

Verified locally

Built against a checkout that has schemas/ and one that does not.

  • /schemas/report/v1.json is byte-identical to the upstream file (cmp)
  • /schemas/report/latest.json is byte-identical to v1.json (cmp)
  • $id in both reads https://flashtrace.github.io/schemas/report/v1.json
  • the top-level field list on /docs/schemas/report/#v1 matches the schema's required array, with forwards correctly shown as rich-mode-conditional
  • every on-page #anchor resolves to a heading that exists
  • strictness and version selection covered by a throwaway harness (12 cases): unknown keywords at root / in $defs / in a property / in a then-branch each report their pointer; unresolvable and remote $ref fail; allOf without if/then fails; v10 sorts above v9; an upstream latest.json and malformed JSON are both rejected

Still 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 _headers support:

curl -I https://flashtrace.github.io/schemas/report/v1.json

Expecting 200, Content-Type: application/json, and Access-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 $id depend on the CORS header. If either is wrong we need a different hosting path for /schemas/, so it is worth checking promptly.

Sequencing

schemas/ is not upstream 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.

🤖 Generated with Claude Code

MentorFilou and others added 2 commits July 20, 2026 14:10
Copies schemas/** from the tool repo to /schemas/**, byte for byte, and
generates a /docs/schemas/<name>/ page per schema from the schema itself.
Both come from the same release checkout that /docs/ is built from.

The files are machine-consumed, so they never touch the markdown pipeline:
the strings inside them ($id, $ref, $schema) are identifiers, not links.

latest.json is a real byte copy of the highest version, not a redirect -
GitHub Pages has no server-side redirects and a meta-refresh means nothing
to a JSON fetch. Its $id keeps pointing at the canonical version, so a
consumer that vendors the file still knows which version it holds. The
highest version is picked by parsing vN numerically, so v10 beats v9.

Version files are re-copied on every build rather than treated as final:
schemaVersion only moves on breaking changes, so v1.json keeps receiving
additive updates within version 1.

The renderer is deliberately strict - an unrecognized keyword fails the
build naming its JSON pointer, because a page that silently omits a
constraint is worse than a missing page. It resolves local $defs pointers
to on-page anchors and walks the top-level allOf alongside the base schema
by instance location, so the rich-mode conditionals land on the fields they
constrain (forwards at the top level, wantedBy on the item definition)
instead of being dropped.

schemas/ is absent until the release that introduces it, so a missing
folder only warns and the rest of the site still builds; once it exists,
anything wrong with its contents is fatal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stacking one section per version does not scale - by v3 the page is mostly
history and the TOC lists three copies of every definition. A picker at the
top defaults to the latest and switches on demand.

Every version still ships in the page and only its display is toggled, so
#v1 and #v1-item keep resolving, in-page $ref links keep working, and the
browser's own find still hits what is on screen.

Degrades cleanly. The .js class now lands on <html> in the pre-paint theme
script, and the latest version is marked current in the markup - so the page
paints on the right version rather than flashing all of them and collapsing
to one. With scripting off the picker is styled away entirely and every
version renders stacked, newest first, with all anchors reachable.

A select rather than tabs: version counts only grow, and a picker stays one
line at ten versions. It is rendered only when there is more than one
version to choose between.

The TOC rail follows the same data-version filter, and the flow marker now
skips sections with no box - a display:none section measures as a zero rect
at the top of the document and would drag the marker up there.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@MentorFilou

Copy link
Copy Markdown
Member Author

Update: version picker replaces the stacked sections

/docs/schemas/<name>/ now opens on the latest version with a picker at the top, instead of rendering every version stacked down the page.

Every version still ships in the page — only its display is toggled — so #v1, #v1-item and the in-page $ref links all keep resolving, and browser find still hits what is on screen. A <select> rather than tabs, because version counts only grow and a picker stays one line at ten versions. It renders only when there is more than one version, so today's single-version page is unchanged.

No-JS behaviour is deliberate, not incidental. The .js class now lands on <html> in the existing pre-paint theme script, and the latest version is marked current in the markup — so the page paints on the correct version rather than flashing all of them and collapsing to one. With scripting off the picker is styled away entirely and every version renders stacked, newest first, all anchors reachable.

One non-obvious interaction: the TOC flow marker caches heading rects, and a display:none section measures as a zero rect at the top of the document, which dragged the marker up there. It now skips sections with no box.

Verified in a headless browser

Against a two-version fixture (a synthetic v2, since only v1 exists upstream), 16 checks — default is latest; picker swaps section, TOC and hash; #v1 / #v2 / #v1-item / #v2-summary each select the right version and scroll into view; the flow marker still tracks a visible section; and with javaScriptEnabled: false the picker is hidden and both versions stay readable. Also confirmed the single-version page is not blank, which is the failure mode a wrong CSS rule would produce. Light and dark both check out.

Heads-up: the schema changed upstream mid-review

47aa719 refactor: give the JSON report a single document shape landed on feat/json-report while this was in progress. The base/rich split is gone: no mode property, no top-level allOf, and forwards and item.wantedBy are now unconditionally required.

The renderer handles it correctly — the page's field list matches the new required array exactly, and all seven top-level properties now show as Required. Nothing needed changing, which is the point of generating the page from the schema.

The conditional (allOf / if-then) support stays in. It is no longer exercised by the real schema, so it is now covered by a synthetic conditional schema in the check harness instead — a future schema may well reintroduce conditionals, and silently dropping one is exactly the failure this renderer is built to prevent.

Worth a sanity check from your side that collapsing the modes was intended and not an in-progress state, since it changes what --json=base consumers can expect.

MentorFilou and others added 3 commits July 20, 2026 15:55
It sat below the intro prose, so on a narrow viewport you scrolled past
version-independent text before discovering the page could be switched.

The right rail was the other candidate and is the wrong home: .toc is
display:none under 1100px, which would strand the only way to change version
on every tablet and phone. Duplicating the control would mean two selects
with one id to keep in sync.

On the title line it is always present, reads as "this page is showing
version N", and sits above everything it governs. The boxed panel competed
with the h1, so it is now just a label and a select, wrapping under the
heading when the two no longer fit. Option labels shorten to v10 (latest) to
fit beside the heading; they match the anchors, and the section heading below
still spells out "Version 10".

Long canonical URLs get overflow-wrap: anywhere. They are unbreakable strings
that pushed the page into a horizontal scroll on a phone - the page now
overflows exactly as much as every other docs page, which is to say only from
the pre-existing topbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A schema is now identified by (directory, format) rather than assumed to be
JSON, so schemas/{name}/v{N}.{format} reads off both: schemas/some-name/v1.json
is titled "Some Name JSON", and a future schemas/report/v1.xml would be
"Report XML" beside it rather than colliding with the JSON page.

The version filename regex captures the extension instead of hardcoding
.json, and the latest alias follows it - latest.xml for xml versions, one per
format. Serving stays format-agnostic: every file is copied verbatim whatever
its extension.

Rendering is not format-agnostic, because schema-doc.mjs reads JSON Schema.
An unrenderable format is still published and warned about rather than
failing the build - the file is served correctly, only its page is missing,
and blocking a release deploy over that is the worse trade. That differs from
the strictness inside a page, where an unknown keyword still aborts: there
the risk is a page that is silently wrong, not merely absent.

Doc pages still live at /docs/schemas/<name>/, which has no room for two
renderable formats of one schema. That is unreachable while only JSON
renders, so it is a guarded error rather than an invented URL scheme.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
/docs/schemas/report-json/ rather than /docs/schemas/report/, so two formats
of one schema get a page each instead of one overwriting the other.

The served path is deliberately not slugged: /schemas/report/v1.json keeps
mirroring the tool repo's directory layout exactly, because that URL is the
schema's $id and a consumer fetches it by that identifier. Only the docs page
- a site concern, not an artifact - takes the qualified slug.

This also retires the collision guard added with the previous commit. Pairing
a directory with its format is injective: a format matches [A-Za-z0-9]+ and
so cannot contain the dash that joins them, meaning two schemas can never
land on one slug.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@MentorFilou

Copy link
Copy Markdown
Member Author

Split into two PRs so the serving half can merge without waiting on a review of the renderer:

Together they build a byte-identical dist/ to this branch (verified with diff -r), so nothing was lost in the split. Closing in favour of those two.

@MentorFilou MentorFilou self-assigned this Jul 20, 2026
@MentorFilou
MentorFilou deleted the feat/schema-publishing branch July 21, 2026 07:40
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.

1 participant