Skip to content

feat(rest)!: retire GET {base}/docs, the OpenAPI document's HTML viewer page; /openapi.json stays - #25124

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-25104-docs-viewer-script
Oct 21, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-25104-docs-viewer-script

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #25104

Clause-②: no (narrowing)
Metadata-protocol: none

What and why

GET {base}/docs, the HTML viewer page for the OpenAPI document, is retired on both route shapes: the unscoped {base}/docs and the environment-scoped {base}/environments/:environmentId/docs. Nothing read it, and the API origin serves no HTML viewer. This is the maintainer's choice on the card, 「C 下线页面」.

GET {base}/openapi.json, its api.enableOpenApi gate and its answer stay. The dispatcher's security-header defaults are not touched.

Contract

Every input whose answer changes, old → new. Measured on this branch's build (packages/rest/dist carries 0 viewer markers and 1 openapi.json registration). Hosts were booted with REST and dispatcher project scoping on, as serve.ts forwards it.

Input Old New
GET {base}/docs, signed-in caller, plugin-hono-server 200 text/html, the viewer page 404 application/json, {"success":false,"error":{"code":"ENDPOINT_NOT_FOUND","message":"Not found"}}
GET {base}/docs, anonymous caller, plugin-hono-server 401 UNAUTHENTICATED the same 404 ENDPOINT_NOT_FOUND
GET {base}/environments/:environmentId/docs, signed-in and anonymous, plugin-hono-server 200 page / 401 the same 404 ENDPOINT_NOT_FOUND
both /docs shapes, signed-in and anonymous, node IHttpServer (the private conformance adapter) 200 page / 401 404 ENDPOINT_NOT_FOUND, the same body
GET /api/v1/docs on a booted showcase (dogfood bootStack), anonymous and member 401 / 200 page 404, byte-identical to the body a never-served path gets on that boot
GET {base}/docs with a session held by an auth policy, or during a permission-store outage that policy's 403 / 503 no handler exists, so the host's not-found applies. Not measured separately on a host.
paths of GET {base}/openapi.json (both shapes) lists {base}/docs does not list it
api.enableOpenApi: false unmounts 2 routes unmounts 1 route, the document
RestServer.getRoutes() one GET {base}/docs per mounted base none

Unchanged, measured on both RestServer hosts and both shapes: GET {base}/openapi.json answers a signed-in caller 200 with the OpenAPI 3.1 document and an anonymous caller 401 UNAUTHENTICATED. A path that was never served answers the same 404 body as the retired path. @objectstack/hono's createHonoApp mounts no RestServer route. It answers 404 ROUTE_NOT_FOUND on /docs, on /openapi.json and on a never-served path, as it did before this change.

The pins

New: packages/rest/src/rest-docs-viewer-retired.pin.test.ts. It asserts the following on both route shapes:

  • {base}/docs has no registration: none in getRoutes(), none in the route manager under any verb, and no path handed to the host IHttpServer.
  • {base}/openapi.json is still registered on the server and on the host.
  • The served document is the OpenAPI 3.1 body. As a control it lists its own openapi.json path, and it lists no /docs path.

Red on the base tree f2b6cf230b with only the pin added (commit 5fb3bdb3f0): Tests 4 failed | 2 passed (6). The four not-served and not-described cases fail; the two kept-document cases pass. Green after the retirement: 6/6.

Updated: what each asserted before, and what it asserts now. No assertion that is still true was loosened.

  • rest-api-description-anonymous-deny.test.ts
    • Before: two endpoints × two bases. Anonymous 401 for the document and the viewer, the viewer's 401 body byte-identical to /data's, the signed-in viewer serving HTML pointed at its sibling document, the document describing both endpoints, and the gated-session 403 and outage 503 on both endpoints.
    • Now: the document only, on both bases, with every document assertion unchanged. The viewer cases left with the viewer, and the new pin holds its absence.
  • rest-config-mount-table.pin.test.ts: the api.enableOpenApi row went from removing GET /api/v1/docs and GET /api/v1/openapi.json to removing GET /api/v1/openapi.json alone. The table still has 20 switches.
  • rest-api-config-dead-keys-refused.test.ts
    • Before: the default mounts both routes the switch owns, and false removes exactly both.
    • Now: the default mounts the document, and false removes exactly it.
  • rest-route-ledger.ts and its conformance test: the GET /api/v1/docs row is removed. The conformance test is unchanged. It still asserts that the ledger and the mounted routes match in both directions, and it went red in the retirement ablation only because the row named a route that was no longer mounted.
  • execctx-consumer-census.test.ts: sites 62 → 61, mentions 145 → 144, bare 46 → 45, caught unchanged at 16. The §3 floor rows ≥ 46 stays at 46: 51 rows were measured after the retirement, so the old floor still holds and is not lowered.
  • html-outlets.pin.test.ts (from PR fix(rest): escape the API docs viewer URL value for its HTML attribute context (ruling 3C) #25093)
    • The outlet list goes from discovery-routes.ts to empty.
    • The non-outlet list drops html-escape.ts (retired) and rest-route-ledger.ts, whose only HTML-touching string was the viewer row's note. It keeps import-template.ts.
    • The census is unchanged.
    • The positive control used to read discovery-routes.ts from disk. It now runs the census over this package's tree plus a fixture source shaped like the retired page, and asserts that the census finds the fixture and that the fixture is exactly the one unclassified file.

Beyond the measured starting map: packages/qa/dogfood. Main's full battery runs these files; PR CI does not. Without these edits they would go red on main after the merge:

  • authz-probe-blind-spot.census.ts
    • Ledger row: 81 / 81 / 0 → 80 / 80 / 0.
    • rest-server.ts row: 70 / 19 / 51 → 69 / 19 / 50. Controls: this.routeManager.register( 63 → 62, discovery-routes.ts 4 → 3, enforceAuth 112 → 111.
    • Totals: 65 / 70 → 64 / 69.
    • All numbers re-derived by its own deriveProbeFileCensus().
  • authz-conformance.matrix.ts: the anonymous-deny-api-description row now covers the one route left, and the docblock's ledger figure goes from 81 to 80 rows, which its pin reads.
  • showcase-anonymous-deny-surfaces.dogfood.test.ts: the viewer row and its 200 member control are replaced by a retired-path case. It asserts 404 for both the anonymous caller and the member, with a body equal to that of a never-served path.

Also in this change

  • packages/rest/src/html-escape.ts is retired with its only caller. git grep escapeHtml over packages/rest finds no other caller. The repo's other escaper is private to plugin-email.
  • Comments that described the page are updated in discovery-routes.ts, rest-server.ts, openapi-builtin-paths.ts and rest-route-ledger.ts. The ADR anchor scripts/adr-anchors/packages__rest__src__discovery-routes.ts.json now names the one API-description door left.
  • QA checklist access-security.anonymous-floor-v18-doors goes to revision 3. /docs leaves the floored-door step, and a new step and clause drive the retired path to the host's ordinary 404, following the file's revision and history convention.
  • The changeset for @objectstack/rest is patch, because this is a non-protocol change (Metadata-protocol: none), and keeps its BREAKING banner and its ADR-0087 not-required (no-migration-prescription) disposition.

Not in this change

Verification (code at 1379d30df4, a merge of origin/main edcc8ceae6; the head 520df1bfa6 adds only the changeset-level fix above it)

  • @objectstack/rest local project, run at the pre-merge head b55b8f782d: Test Files 312 passed (312), Tests 6185 passed | 326 skipped (6511). The merge brought one packages/spec describe-text commit only, so it was not re-run.
  • pnpm --filter @objectstack/rest typecheck exit 0 (tsc --noEmit plus check:test-typecheck). pnpm --filter @objectstack/dogfood typecheck exit 0. Both programs list the edited test files.
  • dogfood, run once on the final head: authz-probe-blind-spot.test.ts and authz-conformance.test.ts 108/108; showcase-anonymous-deny-surfaces.dogfood.test.ts 74/74, including the new retired-path case.
  • @objectstack/spec was rebuilt after the merge. check:generated reports all 14 artifacts up to date.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 1379d30df4 derived 86 commands. All 86 were run, plus the 10 families listed under "Declared WIDE population", and every one exited 0. check:dual-build-cjs-loads first answered PREREQUISITE NOT MET because 8 unrelated packages had no dist; it was re-run after building them (all turbo cache hits) and exited 0. Reconciliation: ✓ dispatch-gates --ran: 86 derived famil(ies) accounted for — 86 run, 0 NOT-MEASURED (a DERIVED zero — all 86 recorded an exit code and none of them is 3).
  • pnpm lint: exit 0 on 1379d30df4 (eslint . --no-inline-config, the whole tree).

Acceptance notes

  • NOT MEASURED: the new dogfood retired-path case against main. Its red direction follows from main serving /docs with 401 to an anonymous caller and 200 to a member, which this file asserted until now.
  • This narrowing owes one CONTRACT_REVIEW_TIER review of record before the queue, as the claim states.

Generated by Claude Code

…and /openapi.json as kept

The pin is red on the current tree (the viewer page is still registered) and
turns green with the retirement in the next commit.

Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6
Co-authored-by: Claude <noreply@anthropic.com>
…er page

The viewer page had no consumer, and the API origin serves no HTML viewer.
GET {base}/openapi.json, its gate and its answer stay. The page's escaper
leaves with its only caller, the outlet census keeps running over a fixture,
and the route ledger row and every pin that named the page move to the
retired state.

Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6
Co-authored-by: Claude <noreply@anthropic.com>
…ocs viewer page

The blind-spot census reads one fewer registration site and ledger row, and
the API-description matrix row covers the one route left. The execctx census
keeps its 46-row floor, which still holds.

Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6
Co-authored-by: Claude <noreply@anthropic.com>
… on a booted showcase

The anonymous-deny surfaces file drops the viewer from its API-description
endpoints and drives the retired path instead: anonymous and signed-in both
get the same 404 body a path nothing ever served gets on that boot.

Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6
Co-authored-by: Claude <noreply@anthropic.com>
…r the retired /docs viewer page

The discovery-routes ADR anchor names the one API-description door left, the
v18 anonymous-floor checklist row drops /docs from its floored doors and
drives the retired path to the host's ordinary 404 (revision 3), the authz
matrix docblock reads 80 ledger rows, and the @objectstack/rest changeset
declares the narrowing.

Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 21, 2026
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/rest, touching 9 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/rest/src/openapi-builtin-paths.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

⛔ 5 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx (via RestServer (symbol, a top-level class))
  • content/docs/releases/v12.mdx (via RestServer (symbol, a top-level class))
  • content/docs/releases/v16.mdx (via RestServer (symbol, a top-level class))
  • content/docs/releases/v17/17-3.mdx (via RestServer (symbol, a top-level class))
  • content/docs/releases/v17/17-5.mdx (via RestServer (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/rest/src/openapi-builtin-paths.ts) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: /api/v1 (route, 100 pages)
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 19 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4e2c42f363a5f1dc45f2aad01fa05dcd423ac58e → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 26ad5305788a1b0eb1f5f97222b1547c76b0a398 — the merge of head 520df1bfa636cb2595168f4137c8df4fe78dadd5 into base 4e2c42f363a5f1dc45f2aad01fa05dcd423ac58e, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 26ad5305788a1b0eb1f5f97222b1547c76b0a398 && git checkout 26ad5305788a1b0eb1f5f97222b1547c76b0a398
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4e2c42f363a5f1dc45f2aad01fa05dcd423ac58e 520df1bfa636cb2595168f4137c8df4fe78dadd5 && git checkout -B drift-repro 4e2c42f363a5f1dc45f2aad01fa05dcd423ac58e && git merge --no-ff 520df1bfa636cb2595168f4137c8df4fe78dadd5

node scripts/docs-audit/affected-docs.mjs --json 4e2c42f363a5f1dc45f2aad01fa05dcd423ac58e

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 4e2c42f363a5f1dc45f2aad01fa05dcd423ac58e → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1379d30df4289aa759c7297d94de51a215b1a12c
Local-runs: none
Scope: packages/rest/src/discovery-routes.ts, packages/rest/src/rest-server.ts, packages/rest/src/openapi-builtin-paths.ts, packages/rest/src/rest-route-ledger.ts, packages/rest/src/html-escape.ts (deleted), packages/rest/src/html-outlets.pin.test.ts, packages/rest/src/rest-docs-viewer-retired.pin.test.ts (new), packages/rest/src/rest-api-description-anonymous-deny.test.ts, packages/rest/src/rest-config-mount-table.pin.test.ts, packages/rest/src/rest-api-config-dead-keys-refused.test.ts, packages/rest/src/execctx-consumer-census.test.ts, packages/qa/dogfood/test/authz-conformance.matrix.ts, packages/qa/dogfood/test/authz-probe-blind-spot.census.ts, packages/qa/dogfood/test/showcase-anonymous-deny-surfaces.dogfood.test.ts + packages/rest/src/rest-route-ledger.conformance.test.ts, packages/rest/src/index.ts, packages/qa/dogfood/test/authz-probe-blind-spot.test.ts, packages/qa/dogfood/test/authz-conformance.test.ts, docs/adr/0076-objectql-core-tiering.md (D12), docs/adr/0056-permission-model-landing-verification.md (D2), docs/adr/0138-guest-model-anonymous-principal-doors-grants-and-organization.md (D2), AGENTS.md (the changeset rule), .github/workflows/pr-automation.yml (Check Changeset), scripts/check-changeset-no-major.mjs (judgeLevel), content/docs/references/api/rest-server.mdx (generated), scripts/check-route-envelope.mjs; prose faces judged in ②: .changeset/25104-rest-docs-viewer-retired.md, docs/qa/platform-checklist/areas/access-security.json, scripts/adr-anchors/packages__rest__src__discovery-routes.ts.json

① Derived judgments

Check-runs on the head, latest per name, read twice (15:26Z and 15:33Z):

  • NOT COMPLETED at the second read: Test Core (2/6), (3/6), (4/6), (5/6), (6/6) — in_progress, NOT MEASURED. Test Core (1/6), Type Check · workspace and Type Check · consumer gates completed success between the two reads.
  • FAILURE: Check Changeset (run 119306858703), level axis not-measured-material: the PR body carries no Metadata-protocol: line and the one introduced entry is @objectstack/rest: minor; judged in ②.
  • success: Build Core, Governed Surface Queue Guard, Spec property liveness, the eight Lint runs, Type Check · source gates and · debt ledger, Check PR Size, Check Documentation Links, Flag docs affected, the card-claims-branch / Part-of / single-writer-path / same-issue checks, Auto Label, filter, and Dogfood Regression Gate (13 s: a path filter — the three dogfood files run in main's battery only, per the report, so PR CI does not exercise them). Skipped (opt-in or path-filtered): 18.x harness, Packed-tarball smoke, Build Docs, Console Pin Gate, Dogfood shards, Dogfood Verify CLI, Temporal Conformance.

Accept-set and public-surface changes the diff implies, each judged:

  1. GET {base}/docs on both route shapes stops being served. The only code hunks in discovery-routes.ts are the removed escapeHtml import and the removed 51-line registration block; no handler, no catch, no special-cased refusal replaces it, so the answer is whatever the host gives any unmounted path. The report measured 404 ENDPOINT_NOT_FOUND on plugin-hono-server and the node IHttpServer, both shapes, anonymous and signed in, byte-identical to a never-served control, and createHonoApp unchanged at 404 ROUTE_NOT_FOUND. The new pin holds absence at registrar level on both shapes under all five verbs (getRoutes, routeManager.get, host paths); the dogfood case holds the never-served control's body on a booted showcase for anonymous and member alike and refuses text/html. The ledger holds no base-level parameter route that could swallow the path. RIGHT; nothing new exists to leak, and 404 versus 401 is the host's standing behaviour for every unmounted path.
  2. GET {base}/openapi.json kept byte-for-byte: the document handler, its registration and the api.enableOpenApi gate in RestServer.registerRoutes (rest-server.ts:4752) are code-unchanged — the only hunks inside the handler are comments (the servers[0] note). paths is produced from getRoutes() by buildBuiltinPaths, so the one lost path is derived, not hand-edited; the new pin controls that the served 3.1 document still lists its own path on each base and lists no /docs path. The anonymous-deny test's document assertions are unchanged on both bases. RIGHT.
  3. api.enableOpenApi now gates one route: mount-table pin and dead-keys pin moved to the one route; CASES.length stays 20. RIGHT.
  4. RestServer.getRoutes() loses one entry per base and the ledger drops GET /api/v1/docs: 81 → 80 route: and family: rows re-counted at the head; the unchanged conformance test holds both directions. RIGHT.
  5. html-escape.ts retired: at the merge-base its only importers were discovery-routes.ts (the viewer) and the pin test's NOT_OUTLETS entry; packages/rest/src/index.ts never exported it; the two other escapeHtml spellings in the tree are private locals of plugin-email and service-messaging. RIGHT; no published surface moves.
  6. Outlet census: the classification case still compares every HTML-touching shipped source against OUTLETS plus NOT_OUTLETS, so a new unclassified file still reds; the positive control adds a fixture source shaped like the retired page to the tree's text map and asserts the census finds it and that it is exactly the one unlisted file. NOT_OUTLETS drops html-escape.ts (gone) and rest-route-ledger.ts, whose only HTML-touching string was the viewer row's note; at the head both delisted files carry the word only in comments, which the census ignores. RIGHT.
  7. Counts, re-derived at the head: dogfood ledger row 81/81/0 → 80/80/0 with RestRouteLedgerEntry 2 unchanged; rest-server.ts row 70/19/51 → 69/19/50 with this.routeManager.register( one fewer (discovery-routes.ts 4 → 3) and enforceAuth one fewer over the registrar files; totals 65/70 → 64/69 keep the pinned runtime-minus-static of 5; the matrix docblock's 81 → 80 rows feeds the [finding] authz-conformance.matrix.ts:27 states the route ledger holds "94 rows / 19 families" in the present tense — measured 91 today, and the cause is #14503, not the gate move it sits next to #17111 figure pin. execctx census 62 → 61 sites, 145 → 144 mentions, 46 → 45 bare (one resolveExecCtx line left with the handler, no prose mention touched), caught 16; the §3 floor rows ≥ 46 kept against 51 measured rows — keeping a floor that holds is not a loosening. RIGHT.
  8. Updated pins loosen nothing still true: the document's 401 body byte-equal to /data's, no work done, the signed-in 3.1 body, the inherited security requirement, the gated-session 403 and the outage 503 all remain and iterate the one endpoint left; only the viewer's assertions left, into the new pin. RIGHT.
  9. ADR anchor invariant: the D12 half (advertise only what is mounted; sole owner of /openapi.json) is unchanged; the ADR-0056 D2 / ADR-0138 D2 sentence narrows from both doors to the one door, consistent with ADR-0138 D2's closed list of six classes, which names no API-description endpoint, and ADR-0056 D2 default-deny. RIGHT.
  10. Fallout outside packages/spec at the head: only content/docs/references/api/rest-server.mdx (lines 248, 260, 296) still names the viewer, and it is AUTO-GENERATED from the rest-server.zod.ts describe text, so it follows spec(api): after #25104 retires GET {base}/docs, rest-server.zod.ts still names the docs viewer (enableOpenApi's TSDoc and describe, and the documentation.enabled tombstone prescription) #25121 (domain:spec) as the PR body says; scripts/check-route-envelope.mjs:701 and authz-probe-blind-spot.census.ts:758 are dated [maintainer] refactor(rest): split packages/rest/src/rest-server.ts (13,701 lines, 70 commits in 30 days) along its register*Endpoints seams, one move-only slice per PR, /meta after #22877 stage 2 #23050 slice-7 history comments, not expectations a gate reads. ACCEPTED, out of this PR by rule.
  11. The merge of origin/main edcc8ce (feat(gates): the changeset level follows the metadata protocol — Metadata-protocol: line, level axis, console refresh at patch, release notes by reader action (ruling A) #25114 ruling A plus one spec describe commit): the three-dot diff equals the branch's own diff exactly (17 files, +371 / -316), so the merge adds nothing of its own. RIGHT.

② Semver level

.changeset/25104-rest-docs-viewer-retired.md declares '@objectstack/rest': minor, Clause-②: no (narrowing), a BREAKING banner, an ADR-0087 not-required (no-migration-prescription) marker and a one-line reader migration. Neither the changeset body nor the PR body carries a Metadata-protocol: line.

The arm is RIGHT: a published route stops being served and nothing widens.

The level is WRONG at this head. The diff moves nothing a metadata author can write — no spec key, accepted value, type, export, config field or response shape — so its kind is Metadata-protocol: none, and under ruling A none takes patch: "a non-protocol breaking change ships patch and KEEPS its carriers: the BREAKING banner and the ADR-0087 disposition" (AGENTS.md, the changeset rule; pr-automation.yml "WHICH LEVEL"; check-changeset-no-major.mjs judgeLevel). Ruling A landed on main at f2b6cf2, 2026-10-21T13:08Z — the very commit the claim amendment 6318003983 says its surface was read on — and is present at the merge-base and at this head. The "launch-window convention for breaking changes → minor" the changeset and PR body cite is the 2026-09-04 rule ruling A amended; only its major refusal survives. The gate on this head reads it the same way: Check Changeset is failure, not-measured-material ("a kind and a none would grade this differently"); declaring the honest none on the changeset as it stands would become none-minor, exit 1, and narrowed would be a false declaration, since no authorable key or value leaves.

Remedy, a push rather than a body edit alone: grade '@objectstack/rest': patch; add Metadata-protocol: none as a bare line beside Clause-②: no (narrowing) in the changeset body and in the PR body; keep the BREAKING banner, the (narrowing) arm, the ADR-0087 marker and the migration line. That is a new head and owes a new record.

Clause-②: no (narrowing)

③ Boundary flags

The dev's deviations:

  • Three packages/qa/dogfood files beyond the claim's surface — ANSWERED: pins of the same class as the five named, every figure re-derived by deriveProbeFileCensus() and the [finding] authz-conformance.matrix.ts:27 states the route ledger holds "94 rows / 19 families" in the present tense — measured 91 today, and the cause is #14503, not the gate move it sits next to #17111 docblock pin; no producer or consumer changed outside packages/rest, and without them main's battery turns red after the merge. Right to include; the PM amends the claim.
  • The ADR anchor text — ANSWERED: it now names the one door left and its D12 / D2 citations hold against the ADRs read.
  • The comment sweeps in rest-server.ts (lines 393, 4938 to 4943) and openapi-builtin-paths.ts (lines 115 to 116) — ANSWERED: comment-only hunks, under the dispatch's "every comment that describes it".
  • The census floor kept at 46 — ANSWERED: 51 rows measured after the retirement; a floor that holds is not lowered, and keeping it is not a loosening.
  • The merge of main before the PR opened — ANSWERED: clean, the net diff identical to the branch's.
  • The second, verbose dogfood run and the scratch host test (never committed) — noted, harmless.
  • NOT MEASURED, the new dogfood retired-path case against main — ACCEPTED: its red direction follows from the 401 / 200 assertions the file carried until this change.
  • The gate report "every one exited 0" was written before CI finished: the head now shows Check Changeset red, because the level axis reads the PR body, which the local dispatch-gates run does not.
  • Attribution form — not this record's question.

open_questions: none in report 6319625250; round 22's single question was answered by the maintainer's choice 6317642533 (C, retire the page and keep /openapi.json), which this diff implements.

ESCALATED to the dispatching seat: the Metadata-protocol: declaration and the patch level (②). Out of this PR by rule and carried by #25121 (domain:spec): the rest-server.zod.ts describe and tombstone prescription, liveness/rest_api.json:73, openapi-self-consistency.ts:34 and :156, migrations/entries/semantic/18.rest-api-config-dead-keys-retired.ts:20 and :46, the dormant uiPath and ApiTestingUiConfigSchema, and the regeneration of content/docs/references/api/rest-server.mdx.

Test Core 2/6 to 6/6 were still running at the second read and are NOT MEASURED here; the @objectstack/rest suite figure (312 files, 6185 passed at the pre-merge head b55b8f7) is the dev's verify-lock report, not a check-run.

Implemented-by: claude/issue-25104-docs-viewer-script
Reviewed-by: session_01LmjU2x1faLZ39DVTFyVca6

VERDICT: FAIL

…rotocol change

The retirement changes what a REST caller is served, not what a metadata
author can write, so the changeset declares Metadata-protocol: none and
ships @objectstack/rest at patch. The BREAKING banner, the ADR-0087
disposition and the migration line stay.

Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 520df1bfa636cb2595168f4137c8df4fe78dadd5
Local-runs: none
Scope: packages/rest/src/discovery-routes.ts, packages/rest/src/rest-server.ts, packages/rest/src/openapi-builtin-paths.ts, packages/rest/src/rest-route-ledger.ts, packages/rest/src/html-escape.ts (deleted), packages/rest/src/html-outlets.pin.test.ts, packages/rest/src/rest-docs-viewer-retired.pin.test.ts (new), packages/rest/src/rest-api-description-anonymous-deny.test.ts, packages/rest/src/rest-config-mount-table.pin.test.ts, packages/rest/src/rest-api-config-dead-keys-refused.test.ts, packages/rest/src/execctx-consumer-census.test.ts, packages/qa/dogfood/test/authz-conformance.matrix.ts, packages/qa/dogfood/test/authz-probe-blind-spot.census.ts, packages/qa/dogfood/test/showcase-anonymous-deny-surfaces.dogfood.test.ts (all fourteen byte-identical to head 1379d30, judged there by record 6319920291 and carried here by citation) + .changeset/25104-rest-docs-viewer-retired.md (at both heads, and the delta between them), the PR body as it stands, the PR file list, the net diff stat against merge-base edcc8ce and the three-dot diff against origin/main 4e2c42f, AGENTS.md on origin/main (Post-Task Checklist step 3, lines 1084 to 1101), .github/workflows/pr-automation.yml on origin/main (WHICH LEVEL lines 754 to 813; the Check Changeset steps at 919 to 920 and 998 to 999), scripts/check-changeset-no-major.mjs on origin/main (judgeLevel 1916 to 2055, the ruling-A self-test pins 3736 to 3746), scripts/check-adr-0087-registration.mjs on origin/main (header 131 to 160, the category set 537 to 542, the fifth signal at 632), scripts/pm/clause2-line.mjs on origin/main (readMetadataProtocolLine 515 to 565), packages/spec/src/api/rest-server.zod.ts on origin/main (lines 144 and 224 to 236, read only to close the semantic-versus-none question), packages/rest/package.json and packages/qa/dogfood/package.json at the head, PR #25114 (ruling A) body and its two comments; card #25104 body and all six comments including round 26's report 6322448018; the previous record 6319920291; the 47 check-runs on this head, read twice (17:38:32Z and 17:41:21Z). The recogniser's --paths reading was run only as the template helper the brief prescribes (it decides no face off the paths and defers to the changeset's Clause-② arm: narrowing face); no gate, build or test was run.

① Derived judgments

The delta, proven. git diff 1379d30df4289aa759c7297d94de51a215b1a12c 520df1bfa636cb2595168f4137c8df4fe78dadd5 touches exactly one path, .changeset/25104-rest-docs-viewer-retired.md, +3 / −2: the frontmatter '@objectstack/rest': minor becomes patch; a line-initial Metadata-protocol: none is inserted directly under Clause-②: no (narrowing); the BREAKING banner's tail changes from "shipped as minor under the launch-window convention for breaking changes" to "shipped as patch because it is a non-protocol change: it changes what a REST caller is served, not what a metadata author can write". The summary line, the adr-0087 HTML-comment marker, the three bullets and the reader migration line are byte-identical. The head is one commit, 520df1b, whose parent is 1379d30 (a fast-forward, no force). The merge-base against origin/main is still edcc8ce, and the net diff is the same 17 files (+372 / −316; the one extra insertion versus the prior record's +371 is the changeset's new line); the three-dot diff against current origin/main 4e2c42f reports the identical 17 files and counts, so nothing from main entered this head. No path under packages/spec/, authorable-surface/ or authorable-defaults/ is in the diff (0 matches).

Carried by citation. Because no non-prose path moved between the two heads, record 6319920291's ① judgments 1 to 11 on head 1379d30 carry whole to this head: the route retirement on both shapes with the host's own not-found answer (1); /openapi.json, its gate and its answer code-unchanged, paths derived not hand-edited (2); api.enableOpenApi gating one route (3); the ledger 81 to 80 both directions (4); html-escape.ts never exported (5); the outlet census kept, positive control on a fixture (6); every dogfood and execctx count re-derived, the floor kept (7); no still-true assertion loosened (8); the ADR anchor against ADR-0076 D12, ADR-0056 D2 and ADR-0138 D2 (9); the packages/spec residue out of this PR by rule, carried by #25121 (10); the merge of main adding nothing (11). Every one was RIGHT there and nothing in this delta can move them.

Check-runs on this head, latest run per name, second read at 2026-10-21T17:41:21Z (47 runs):

  • NOT COMPLETED, NOT MEASURED: Test Core (1/6) to (6/6), Type Check · workspace, Type Check · consumer gates — all in_progress at both reads. Judged on the diff: the delta is one .changeset/*.md file that no vitest project and no tsc program reads, so this delta cannot move either gate; their verdict on this head is still theirs to give, and the landing seat reads them success before the queue (a running gate is not a pass).
  • success (23): Check Changeset, Lint & Repo Gates, all eight Lint · fan-outs, Build Core, Governed Surface Queue Guard, Type Check · source gates, Type Check · debt ledger, Spec property liveness, Dogfood Regression Gate (the path filter; the three dogfood files run in main's battery only), Check Documentation Links, Flag docs affected, the card-claims-branch / same-issue / single-writer-path / Part-of checks, filter.
  • skipped (9, opt-in or path-filtered): 18.x harness, Packed-tarball smoke, Build Docs, Console Pin Gate, Dogfood shards, Dogfood Verify CLI, Temporal Conformance, Check PR Size and Auto Label on the edited event.
  • Check Changeset has two runs on this head: 119378021481 (workflow run created 17:34:18Z on the push) concluded failure and is superseded by 119378384408 (created 17:34:59Z, the edited event after the body patch), success, the latest per name — the body is re-read on edited, which is that gate's documented path for a body-cleared red. That job is also where the real check-adr-0087-registration --base MERGE_BASE scan and check-empty-changeset run (pr-automation.yml 919 to 920, 998 to 999; the Lint lane carries only their self-tests), so its green covers the level axis, the ADR-0087 disposition and the empty-changeset check together.

② Semver level

Judged afresh against AGENTS.md Post-Task Checklist step 3 on main (ruling A, #25114, landed f2b6cf2 at 13:08Z, an ancestor of the merge-base and of this head):

  • '@objectstack/rest': patch — RIGHT. The only published package the diff moves is @objectstack/rest (18.0.0, not private); @objectstack/dogfood is private: true; docs/qa/** and scripts/adr-anchors/** publish nothing. One entry, and the kind decides its level.
  • Clause-②: no (narrowing) — RIGHT, unchanged: a published route stops being served and nothing widens (carried ①.1 to ①.3).
  • Metadata-protocol: none — RIGHT. Nothing a metadata author can write changes: no key added, removed or tombstoned, no accepted value refused, no requirement tightened, no meaning or default changed, no metadata type; no shard under authorable-surface/ or authorable-defaults/ moves. The one authorable key in reach, api.enableOpenApi (rest-server.zod.ts:144, boolean, default true, embedder-only), keeps its accept set, its default and its intent; the route set that switch covers is a runtime fact, which ruling A's table files under none ("REST ... fix!: alike"), and its describe text still naming the viewer is "description or error text" — also none, and spec(api): after #25104 retires GET {base}/docs, rest-server.zod.ts still names the docs viewer (enableOpenApi's TSDoc and describe, and the documentation.enabled tombstone prescription) #25121's to regenerate. narrowed would be false (no key or value leaves) and would anyway be the right kind only for a protocol narrowing; semantic would be false (no authorable key's meaning or default moves). Under judgeLevel this is none, one introduced entry at patch, no baseline moved: none-clean, exit 0 — the gate's own ruling-A pin is this exact shape (Clause-②: no (narrowing) with Metadata-protocol: none → none-clean), and Check Changeset is success on the head.
  • The BREAKING banner — kept, and its new wording is TRUE. The code hunks are the removal of one route registration in packages/rest (what a REST caller is served); nothing under packages/spec/ moves and no authorable key, value, default or type changes (what a metadata author can write). The amended "launch-window convention → minor" sentence is gone, as ruling A requires; only the major refusal of that convention survives, and this changeset does not touch it. Describing the ruling in words rather than by name is the right form for text that ships to consumers as CHANGELOG.
  • ADR-0087 disposition not-required (no-migration-prescription) — RIGHT, judged afresh against the gate's own definition: that category is refused when the changeset body carries a migration prescription, instructions for rewriting a consumer's code. The body's reader line ("If you opened the page, load the document from GET {base}/openapi.json into an OpenAPI viewer of your choice") is a human reader's action, not a FROM → TO rewrite of any key, export or config field, and objectstack migrate meta has nothing to apply; the marker's prose closes the other five categories on facts (publishes, so not unpublished; no id, so neither registered nor already-registered; no exported interface, type or export retired — html-escape.ts was never exported, carried ①.5). The kind none adds no breaking signal; the ! summary, the banner and the (narrowing) arm are the signals the gate reads, so the disposition is owed and present. The real scan ran inside Check Changeset on this head: success.
  • Migration line — present and unchanged: the one-line reader fix stands under "What changes for you."
  • The PR body's two declaration lines — RIGHT. Lines 3 and 4 of the body, line-initial, bare values: Clause-②: no (narrowing) and Metadata-protocol: none, matching the changeset's two lines token for token; the "Also in this change" bullet now states patch, non-protocol, with the banner and disposition kept; the acceptance note still says this narrowing owes one CONTRACT_REVIEW_TIER record before the queue.

Clause-②: no (narrowing)
Metadata-protocol: none

③ Boundary flags

Round 26's deviations (report 6322448018), each answered:

  • Round 24's level came from a stale AGENTS.md read in the shared checkout at 795ff70, before ruling A — ANSWERED: named honestly, it is the whole cause of the prior FAIL, and this head is the fix; rules were read at the branch head this round, which is the right practice. For the seat: a dev reads the rule on the tree it builds against, not on the shared checkout.
  • The banner describes the ruling in words, not by name — ANSWERED: the right form for consumer-facing text, and the words are true (②).
  • The worktree rebuilt and removed after the push — noted, harmless.

The prior record's escalation (the Metadata-protocol: declaration and the patch level) — ANSWERED by this head: both lines in both carriers, patch, the banner, the arm, the marker and the migration line kept; Check Changeset green on the head via the edited re-run.

Still out of this PR by rule and carried by #25121 (domain:spec): the api.enableOpenApi describe and the api.documentation.enabled tombstone prescription in rest-server.zod.ts, liveness/rest_api.json:73, openapi-self-consistency.ts:34 and :156, the semantic migration entry's prose, the dormant uiPath and ApiTestingUiConfigSchema, and the regeneration of content/docs/references/api/rest-server.mdx — unchanged since the prior record.

One prose nit, not a verdict: the PR body's "Verification" heading still names 1379d30df4 as the final commit. The head is one changeset-only commit above it, so every cited figure is still the figure for this code, but the heading is stale by one commit; a body edit, no push.

open_questions: none in round 26; none open on the card.

For the landing seat, not for this verdict: eight gates were still running at 17:41:21Z (Test Core ×6, Type Check · workspace, Type Check · consumer gates). This record judges the contract; "every check is green" is read separately before the queue, and in_progress is not green. The PR is a draft with auto-merge unarmed and head repo equal to base repo, as a narrowing awaiting its record should be.

Implemented-by: claude/issue-25104-docs-viewer-script
Reviewed-by: session_01LmjU2x1faLZ39DVTFyVca6

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT

domain:cli seat 2 (#22648) · os-justin · session session_01LmjU2x1faLZ39DVTFyVca6 · 2026-10-21T17:47Z · head 520df1bfa636cb2595168f4137c8df4fe78dadd5 · claim 6315391372 (amended 6318003983) → reports 6319625250, 6322448018 → review of record 6322627375 (CONTRACT_REVIEW_TIER, PASS; it supersedes 6319920291, FAIL on the level only).

Check Reading
Shape Draft, base main. First line Fixes #25104. Clause-②: no (narrowing) and Metadata-protocol: none are line-initial in the body and the changeset. One changeset, @objectstack/rest: patch, with the BREAKING banner, the ADR-0087 not-required (no-migration-prescription) marker and the reader migration line.
Surface 17 files, +372 / −316. check-governed-merges.mjs --pr 25124 on main 4e2c42f363: NOT governed. git merge-tree against that main: clean.
Mechanism GET {base}/docs is retired on both route shapes and answers the host's ordinary not-found. /openapi.json, its gate and its answer are code-unchanged. html-escape.ts is retired with its only caller. The outlet census stays, with a fixture-based positive control. The ledger, the five named pins and the three qa/dogfood pins move to the retired state, and no still-true assertion is loosened (record 6319920291 ① 1–11, carried by 6322627375).
Evidence The new not-served pin is 4 red / 2 green on main and green after. @objectstack/rest: 312 files / 6185 tests. Dogfood: 108/108 and 74/74.
Gates Local: 86/86 derived and the 10 Declared WIDE population families exit 0; pnpm lint exit 0; both changeset gates exit 0 on the new head. Check-runs on this head are being read. Landing waits on every one.

Accepted deviations:

  • Three packages/qa/dogfood/test/ pins and the ADR anchor text beyond the claim: same class, every figure re-derived. This ACCEPT amends the claim's file surface to the PR's 17 files.
  • The census floor is kept at 46: a floor that still holds is not lowered.
  • The body's Verification heading is corrected by this seat to name both heads (a prose edit, no push).

Out-of-scope dispositions (one line each):

Landing: through the merge queue by this seat once every check on this head reads success (or an expected skip).


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 21, 2026 18:06
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 21, 2026
Merged via the queue into main with commit 1b3b65e Oct 21, 2026
55 of 57 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-25104-docs-viewer-script branch October 21, 2026 18:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

rest: the API docs viewer page loads a third-party script by an unpinned URL with no integrity check

2 participants