Uh oh!
There was an error while loading. Please reload this page.
fix(cli): the startup banner names a contested flow name and says which definition is armed (#12028) - #12562
Conversation
📓 Docs Drift CheckThis PR changes 1 package(s): 17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 23 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 5c6320507722310ca85b93e156a85507a0b10da3 && git checkout 5c6320507722310ca85b93e156a85507a0b10da3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5fbd58e0d6f100d1221fd2e3b04eb824144c7e88 6077c4b057b4112d0795171732b273b94e42ef55 && git checkout -B drift-repro 5fbd58e0d6f100d1221fd2e3b04eb824144c7e88 && git merge --no-ff 6077c4b057b4112d0795171732b273b94e42ef55
node scripts/docs-audit/affected-docs.mjs --json 5fbd58e0d6f100d1221fd2e3b04eb824144c7e88
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#12028
What was wrong
collectAutomationSummarybuilds theos dev/os startbanner by reading binding STATE off the live automation engine, because a flow that failed to arm emits no log line to go looking for and the boot-quiet stdout window swallows the engine's ownwarnnarration. It was silent about the one failure it could not express as a count.The engine's flow map is keyed by BARE name. When a packaged flow and a runtime-authored
sys_metadataoverlay both claim one name, ADR-0005 precedence arms one and the loser is not in the map — so it is not inlistFlows(), not ingetFlowRuntimeStates()'s rows, and therefore not in any number this banner prints.3 flow(s), 3 bound to triggerswas a true sentence about a set that did not contain the definition the operator had just edited.#11997 gave the engine the receipt —
AutomationEngine.getShadowedFlows()(packages/services/service-automation/src/engine.ts:2820) plusarmedFrom/shadowedper runtime-state row (:2750). The automation plugin warns from it atkernel:bootstrapped, but that is alogger.warn, which is exactly the channel this banner exists to work around.serve.tsread neither.What this does
collectAutomationSummaryreads the receipt and the banner prints one line per contested name:Naming which body is armed is the point. A line reporting only the count tells an admin something is wrong and withholds the answer they are standing there to get.
Silent on every healthy boot: no contested name, no line.
Measurements behind the shape
The sibling probe, measured before copying it. The
unboundprobe atserve.ts:5197IS feature-detected — optional call,?? [],try/catchcarrying the/* older engine */comment — and the function's docblock states the invariant for every probe in it. The new probe matches those three elements and adds nothing:No cast past the
statesannotation. The local annotation now namesarmedFrom/shadowedalongside the pre-existing fields, so a later read of those rows is type-checked against the row's real shape instead of casting throughany.Where the receipt is read from, and why — a fork, declared rather than picked quietly. The two facts are available from two places:
getShadowedFlows()and thestatesrows. They are information-equivalent except in one direction, which decided it:getFlowRuntimeStates()can only attach a receipt to a row it is already emitting (a name still in the flow map), whilegetShadowedFlows()returns every receipt the boot pull recorded. So the dedicated probe is the source, and the extendedstatesannotation is carried as a declaration rather than as a second read — deliberately not a fallback, since a fallback would be tolerance the sibling probe does not have. Naming this explicitly because it is the one place the card's suggested shape and the "extend the annotation" direction do not describe the same implementation.The type is structural, not imported.
@objectstack/service-automationis a workspace dep ofpackages/cliand exportsFlowContender/FlowShadowingRecord, soimport typewas available. It is deliberately not used: the probes here are feature-detected precisely so a host on an OLDER automation package still boots its banner, and typing these reads against the CURRENT package would state a guarantee the runtime does not make. Every other shape this function reads off the engine is annotated structurally for the same reason.Benign-case filter. A receipt that displaced nothing is not a contested name, and
collectAutomationSummarydrops it. The engine already refuses to record one; the banner keeps its own end of that guarantee locally, where it is testable, rather than inheriting it.Tests — pinned on what the banner RENDERS
packages/cli/src/commands/serve-automation-shadowing.test.ts(new, 13 pins). Every render pin drives a shadowing receipt through the realcollectAutomationSummaryand the realprintServerReadyand reads the stderr line an operator sees — the shapeformat.seed-summary.test.tsandserve-organizations-message-spelling.test.tsalready use on this surface. Asserting only that a field was read would pass against a banner that prints nothing, which is the defect.Three of the pins assert an ABSENCE: a healthy tree, a receipt that displaced nothing, and an engine that is not enabled at all must each produce no line, while the ordinary
Flows:row still prints (so the absence is the line being withheld, not the banner being off).Both directions were ablated at
6077c4b05, each leg with anEXIT INT TERMrestore trap, absolute paths, and the mutation confirmed on disk by grep count and blob-hash comparison before any verdict was read:is claimed byoccurrences 1 to 0, hash changed): 5 failed / 8 passed. Exactly the five positive render pins died. The three absence pins stayed green — which is why the positive pins have to exist.Restore verified by observation, not exit code:
git diff HEADempty and the working blob hash equal to the HEAD blob.Verification — all at
6077c4b05pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2over every test file that importsprintServerReadyorcollectAutomationSummary(8 files):Test Files 8 passed (8),Tests 63 passed (63).pnpm --filter @objectstack/cli typecheck: exit 0.tsc --listFilesconfirms the new test file andformat.tsare both in the program (1 hit each) —packages/cli/tsconfig.jsonincludessrcwith no test exclusion, so "typecheck clean" really does cover the new tests.pnpm lint(repo-wideeslint . --no-inline-config): exit 0. Not narrowed.check:nul-bytes,check:engine-double-contract,check:where-matcher,check:cross-package-test-inputs,check:query-options-erasure,check:test-source-alias,check:route-envelope,check:type-check-coverage,check:changeset-gate-self-tests,check:empty-changeset,check:objectui-changeset,check:i18n,check:i18n-coverage— all 0.check:i18n-coverageprintsOK (12 config(s), 602 baselined untranslated string(s), none new).packages/clisuite is NOT MEASURED here. It exceeded the container's ~10-minute foreground window and was SIGTERM-killed (exit 143) with no verdict written — recorded as unmeasured, never as a pass. The 8 files above are the complete consumer set of the changed surface (grep -rlnfor both symbols acrosspackages/cli/src), and CI runs the whole suite.Both automation package builds were done under the shared verify lock before any verdict was read, so nothing was judged against a stale
dist.Generated by Claude Code