The public site shipped a Releases page and a footer link to a release, and
neither could ever have content: the read path existed, the write path did not.
This adds the seven release operations to the contract contribution and the
gateway, and a Studio screen that can actually write one.
The editor is six markdown fields rather than one, because that is what the
contract models and what a release note is — why, what, what a reader notices,
what it leaves in the code, how it was verified, what is still open. Publishing
is separate from saving: a draft saves in any state, but the public query keys
on PUBLISHED alone, so publish is where completeness is demanded.
No new CSS. The screen reuses the document editor's field classes and the
working-copy list's row classes, so it inherits Studio's spacing and type
instead of introducing a second look.
The footer previously linked `/releases/0.1.0` — a version that did not exist,
so the link 404'd, and one that would have gone stale at 0.2.0 anyway. It now
points at the changelog index, which is the only place that knows what the
latest release is and which reads correctly when there are none.
Route inventory, navigation order, message catalog, manual accessibility
evidence, artifact baseline, and the pinned gate-shape digest all move with the
new route. The digest was recomputed by first reproducing the previous constant
from the previous gates.json, so the computation is known to be the one it was
pinned under.
Publishing needs a topic and nothing could create one. The backend now owns
that surface; this is its consumer — the management contract vendored, a
gateway over its nine operations, and one Studio screen that lists, creates,
and deletes topics and projects.
The screen adds no CSS. It reuses the classes the working-copy list already
uses, so it inherits Studio's spacing, type, and colour rather than growing a
second visual vocabulary beside them. Scope stops at list/create/delete:
renaming, phase changes, and visibility are implemented in the backend and
declared in the contract, but their screens are a separate design.
Two real defects surfaced while making the public port async, and both would
have shipped:
The search page and the header search dialog shared a query key. With an empty
query, `["tech-log","search",""]` was identical for both, so react-query
handed one surface the other's cache — different shapes — and the page died
reading a field that was not there. Keys now name the surface.
The explore filter's selects are uncontrolled and read `defaultValue`, which
React applies once. Their options arrive later now, so the first render had
nothing to match and the value stayed empty: a topic in the URL no longer
showed as selected. The form key includes whether the catalog has arrived, so
it remounts with the options present. Controlled inputs would be the other
answer, but this form submits to build a URL — the URL owns the value.
The route brought its own bookkeeping: a build chunk, a manual accessibility
evidence file, and the CI artifact baseline that counts them. The gate pins a
digest of its own shape precisely so a new route cannot slip in without that
count being reviewed.
Test harnesses that render public screens now assemble the query providers and
await the settled paint, because the screens they render became async.
Three defects found while running the release checklist against a live
backend, all on main.
1. Every TechLog route registered `access: "public"`, including the whole
Studio surface. `decideRouteAccessForDefinition` was therefore a no-op
for Studio: a signed-out visitor who typed /studio, /studio/documents,
or /studio/assets got the Studio shell rendered, and the page went on
to issue Studio API calls. Access is now derived from the spec's own
`layoutGroup`, so a newly added Studio route is gated by construction
rather than by remembering to restate it.
Verified against a production-profile build: /studio* now renders the
sign-in surface, / and /explore are unchanged, and after signing in
the router returns to the originally requested Studio screen.
2. `public/release-manifest.json` still declared the contract set at
2.0.0 while the vendored contract had moved to 3.0.0 (eb86708). Boot
verification fails closed on that mismatch, so `pnpm dev` served a
blank screen. Regenerated from the same producer `dist/` uses.
3. `release-manifest.test.ts` asserted the same stale 2.0.0. The literal
is deliberately independent of `EXPECTED_CONTRACT_SET_PACKAGES` (see
the comment above it), so it is updated in place, not derived.
Also drops a dead `= null` initializer that failed `no-useless-assignment`.
check:types, lint, check:architecture, check:tech-log-contract and
check:dev-release-manifest all pass. test:all is 1818 passed with one
pre-existing load-dependent flake (provider-guardian-transaction, passes
in isolation, untouched by this change).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds TECH_LOG_STUDIO_ASSETS (/studio/assets) as a route reachable but
excluded from primary Studio navigation (navigationLabel/navigationOrder
null), plus the AssetLibrary screen that lists assets, shows usage, and
lets an operator archive or hard-delete one. canHardDelete() is a pure
gate mirroring the server's ASSET_IN_USE rule so the screen never offers
an action the server would refuse.
Adding a 28th route also required updating the route-scoped CI
accessibility-evidence gate (FE-GATE-009 in config/ci/gates.json, plus
its authority-baseline counts and shape digest in
scripts/contracts/ci-gates.ts) and the Vite route-to-chunk map that
scripts/generate-build-manifest.ts depends on, or test:unit and the
production build both fail. See task-11-report.md for the full
breakdown.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>