Repository navigation
fix(cli): a refused Console mount answers 503 naming the remedy, not a bare 404 - #22562
Conversation
…a bare 404 os dev refuses a Console whose built dist/.objectui-sha differs from the repo pin, and an install whose @objectstack/console does not resolve mounts nothing either; both answered the router's bare 404 at /_console/ and /. Both boots now mount a responder on the not-built answer's routes: 503 at /_console/* with the boot message's own remedy text (no absolute host path) and / redirecting to /_console/. - utils/console.ts: one route-mounting body (createConsoleUnavailablePlugin) behind createConsoleNotBuiltPlugin and the new createConsoleShaDriftRefusalPlugin; the drift refusal's text is rendered from one source for the terminal and HTTP; the not-built picker gains a null arm for an unresolved package (same reinstall sentence). - commands/serve.ts: the Console mount branch mounts the drift responder on a refused boot and the not-built responder when the package is null. - console-route-ledger.ts: family and notes of GET /, GET /_console and GET /_console/* name every responder that mounts them. Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check8 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 28 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a95cbe2425551c16a082a4827d703edc6bdf38ff && git checkout a95cbe2425551c16a082a4827d703edc6bdf38ff
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6a3f82efa7d588267eb2e52c06a73c56315c636a 9cadb71c0034a9856e76f6eacd6f6afdc13f48ff && git checkout -B drift-repro 6a3f82efa7d588267eb2e52c06a73c56315c636a && git merge --no-ff 9cadb71c0034a9856e76f6eacd6f6afdc13f48ff
node scripts/docs-audit/affected-docs.mjs --json 6a3f82efa7d588267eb2e52c06a73c56315c636a |
Fixes #22420
Clause-②: no
A refused Console mount answers
503naming the remedy instead of the router's bare404on the same routes, as the not-built position already does (PR #22419,Clause-②: no). No accepted input, key or export changes.What changes
os servesometimes decides not to serve the Console. On two of those boots, the HTTP surface used to say nothing:GET /_console/andGET /answered the router's bare404 ENDPOINT_NOT_FOUND, which looks the same as a wrong URL. Both boots now mount a responder on the routes the not-built answer (PR #22419) already uses:GET /_console/*answers503,text/plain,no-store. The body is the remedy the boot message prints, rendered without absolute host paths.GET /andGET /_consoleredirect to/_console/, as they do when the Console is served.dist/formatConsoleDistMissingWarning503+ redirects (PR #22419)dist/.objectui-shais not the repo pinformatConsoleShaDriftRefusal(byte-identical)404503namingpnpm objectui:build, both SHAs andOS_ALLOW_CONSOLE_DRIFT=1; the pin file is named without its absolute path;/redirectsconsolePathnull, Console enabled)404Console package `@objectstack/console` not found, then the reinstall sentence);503saying the Console is not installed;/redirects--no-ui/--no-console404404, by designFiles:
packages/cli/src/utils/console.ts:createConsoleUnavailablePlugin(not exported) is now the ONE route-mounting body. Before, that body was inline increateConsoleNotBuiltPlugin.createConsoleNotBuiltPlugin(consolePath: string | null)is unchanged except that it now acceptsnull.createConsoleShaDriftRefusalPlugin(drift)is new.describeConsoleShaDrift(drift, hostPaths)(not exported) is the drift refusal's text below its headline. Both the terminal block and the HTTP answer render it.describeConsoleDistMissing(the not-built picker) gains anullarm.packages/cli/src/commands/serve.ts, Console mount branch only:else if (consoleEnabled), so it also coversconsolePath === null.packages/cli/src/utils/console-route-ledger.ts: thefamily(nowconsole-static | console-unavailable) and thenoteofGET /,GET /_consoleandGET /_console/*name every responder that mounts them.console.sha-drift.test.ts: the drift responder on a real Hono app, from a fixture tree with a made-up pin and stamp.console.not-built.test.ts: thenullposition.test/published-subpath-console.pin.test.ts:INTERNAL_SINCE_RETIREMENTrecords the new internal export. The module's own partition pin requires this. The./consolebarrel still publishes exactly three names..changeset/22420-console-drift-responder.md:@objectstack/clipatch.Mechanism hypotheses, measured
H1, confirmed. The branch at
serve.ts:4961(base4638625e07) printedformatConsoleShaDriftRefusalonrefusedForDrift && consoleDriftand mounted nothing. The no-dist arm mountedcreateConsoleNotBuiltPlugin(consolePath). Measured live (below): drift answered404 ENDPOINT_NOT_FOUNDat/_console/,/and/_console.H2, done as "a sibling that shares its route mounting". There is one route-mounting body. Each position has one picker:
describeConsoleDistMissing;describeConsoleShaDrift.The terminal and HTTP forms render the same picker output. The not-built warning, the drift refusal and the drift advisory render byte-identical to base: a script printed all three for a fixed drift from a BASE worktree and from HEAD, and the two outputs have the same sha256,
a9a832a6aa7d….H3, confirmed. The refusal names
drift.pinFile, an absolute path. The HTTP form writes.objectui-sha at the checkout rootinstead, and the terminal keeps the path. The SHAs stay in both, because they are commits of the public objectui repository. Ablation: making the HTTP form namedrift.pinFileturns exactly one pin red, "with only the absolute pin-file path left out" (1 failed / 13 passed). The mutation and its restore were both proven on disk byscripts/ablation-replace.mjs(blobe8dd212b0e75→5c5be9625144→ back toe8dd212b0e75,git diff HEADempty).H4, covered in this PR, with no second remedy picker. When
consolePathisnull, the install is one step past an install with nodist/:@objectstack/consoleis a hard dependency of@objectstack/cli. The existing picker's install arm already names the fix for that ("reinstall it at the same version as the CLI"). SodescribeConsoleDistMissinggains anullarm. It changes only what it says is missing (Console package ... not foundinstead ofConsole dist not found), and its reinstall sentence is the same; a pin compares the two tails. The responder is the samecreateConsoleNotBuiltPlugin, with the headline "is not installed".packages/console. Measured: a fixture with no console package in itsnode_modulesstill resolved the workspace package, and the boot took the not-built arm.createConsoleNotBuiltPlugin(null)on a real Hono app).Live measurement: before and after
The harness boots
bin/run-dev.js serve --devthrough tsx, using anOS_ARTIFACT_PATHartifact from a scratch project. That project'snode_modules/@objectstack/consoleis a fixture dist, and its own.objectui-shais a fixture pin (aaaaaaaaaaaa…, stampbbbbbbbbbbbb…). The live pin is never used. Each child was stopped by PID.GET /_console/GET /GET /_console4638625e07404ENDPOINT_NOT_FOUNDJSON404ENDPOINT_NOT_FOUND4049cadb71c00503text/plainno-store, refusal text, 0 occurrences of the scratch path302to/_console/302to/_console/200SPA shell302302--no-console(control), base and head404ENDPOINT_NOT_FOUND404404dist/(neighbour), head503not-built answer302302The drift answer on head:
Tests and gates
All readings below are against head
9cadb71c00, and every lock-held run printedVERDICT command-exit 0unless stated otherwise.Red against base.
console.tswas restored to4638625e07(blob0a8b6a55bd46proven on disk, then restored by trap to the HEAD blobe8dd212b0e75, withgit diff HEADempty). Then 2 files failed and 11 new pins never passed:createConsoleShaDriftRefusalPlugin is not a function;nullblock failed withThe "path" argument must be of type string. Received null.The 18 pre-existing tests stayed green.
New and touched pins:
console.not-built.test.ts,console.sha-drift.test.ts,console-route-ledger.conformance.test.tsandtest/console-resolve.test.tspassed, 4 files and 62 tests.test/published-subpath-console.pin.test.ts(whose partition check reads the packed.d.ts),test/vitest-tiers-partition.test.tsand the ledger conformance passed, 3 files and 54 tests.cli:
pnpm --filter @objectstack/cli typecheckpassed:tsc --noEmit, andcheck:test-typecheckontsconfig.test.jsonwith its debt unchanged (3 files, 28 errors held).cli unit layer (
vitest run --project unit): 276 files, 4086 tests passed.cli integration layer (
--project integration, run locally becauseserve.tsis a spawn entry): 102 files, 945 passed and 2 skipped.Dogfood. The one dogfood file that reads
/_console,showcase-public-form-redirect.dogfood.test.ts, importsutils/console.tsas source, so it ran locally: 9 of 9 passed. No other file underpackages/qa/dogfoodnames_console, pergit grep.Lint, a proven narrowing (the full
pnpm lintis CI's):--print-configresolves all 6 touched.tsfiles into linted config objects with 5 or 6 rules each.--format jsonoutput reports 6 files, 0 errors and 0 warnings.eslint.config.mjsnever enables type-aware linting (noparserOptions.project, no typed rules), and its local plugins are per-file AST rules. So this diff cannot change the verdict on any untouched file.Gates:
node scripts/pm/dispatch-gates.mjs --commands, re-derived at9cadb71c00with no paths, lists 67 commands: the dispatched 66 pluspnpm check:cli-test-child-env, because of the pin-test edit.--ranreconciliation: "67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN".check:i18n-coveragefirst refused with exit 3 ("COULD NOT MEASURE", because@objectstack/connector-slackwas unbuilt). After the build it named, it re-ran: "OK (13 config(s), 621 baselined untranslated string(s), none new)".check:type-check-debtandcheck:dual-build-cjs-loadsran last, after every locked suite, and both passed.main:origin/main(6a3f82efa7) has moved since the base, but no new commit touches a file in this diff, andgit merge-treeis clean. So there is no merge (per the fence).Acceptance notes
console-route-ledger.conformance.test.tssays both factories inutils/console.ts"return early … unless a resolvable HTTP server AND a builtdist/exist". That has been false for the not-built factory since PR fix(cli): with no built Console, /_console/ answers 503 naming the remedy instead of a bare 404 #22419, and it is false for the drift responder here. The census reads source, so the claim does not affect any verdict.2ba4329e.ConsoleUIis loaded (serve.ts, outside the fence). That matches the not-built position and is unchanged.GET /forms/:slug. It is still mounted only with a served Console, so on any refused boot it falls through to404. This is PR fix(cli): with no built Console, /_console/ answers 503 naming the remedy instead of a bare 404 #22419's observation, unchanged.serve.ts's wiring is proven by the live before/after table above. A spawning pin would put one moreserveboot into the per-PR integration layer (or into the nightly tier, if it were named.e2e).Generated by Claude Code