docs(spec): the residual broken-sweep mentions in automation are a first FILTER, and the misspelled-effect sentences state the measured direction - #13096
Conversation
…rst FILTER, and the misspelled-effect sentences state the measured direction #12685 measured that `selected > 0 AND acted = 0 AND unmeasured = 0` cannot separate a healthy idempotent sweep from a dead gate. #12721, #12722, #12900 and #13068 each rewrote the surfaces that stated it as a detector, and each was fenced to the doc blocks its dispatch named -- so three mentions inside `packages/spec/src/automation` were left standing rather than corrected in passing. One of them ships in `.d.ts` and is what a platform author reads. Reworded to the shape the sibling surfaces now agree on: the predicate is the FIRST FILTER and not a verdict, the per-node fold (`FlowRunSummary.nodes[]` / `gates[]`) is the discriminator, and each clause keeps its own true point. 1. `execution.zod.ts` -- the `unmeasuredEffect` rationale no longer says an understated `0` "fires the broken-sweep alert on a healthy run until operators learn to ignore it". That muting is not peculiar to an understated `0`: after #12685 the filter selects every healthy idempotent sweep. The block now states what a fabricated count really costs -- an understated `0` puts a run that DID act inside the filter, an overstated `1` keeps a run that acted on nothing outside it, and a faked `acted` is a fact the per-node fold can only repeat rather than settle. 2. `execution.test.ts` -- the comment no longer says "the broken-sweep query has to be able to tell". The assertion under it is unchanged and still correct: it pins that `unmeasured` is carried distinctly from `acted`. 3. `flow-function.test.ts` -- the comment mirroring the source sentence #13068 reworded now matches it again. CAUSAL DIRECTION, measured rather than ruled. The card's second half asked whether the "stays silent on the one flow that needed it" sentences state their direction backwards. They do. Read forward: a lost `effect: 'writes'` declaration means the `script` executor reports no `unmeasuredEffect` (`screen-nodes.ts`: `const unmeasured = registration.effect === 'writes'`), so the run folds to `selected > 0, acted 0, unmeasured 0` -- which SATISFIES the three-clause filter. The run lands INSIDE the candidate set reading exactly like a dead sweep, rather than escaping it. The same file's `@module` block already said so ("which reads exactly like the broken sweep #4354 exists to detect"), and `packages/qa/dogfood/test/flow-function-effect.dogfood.test.ts` asserts both legs end to end: declared writer -> `unmeasured: 1` (filter does not match), pure -> `unmeasured: 0` with the filter "free to fire on this run". The card's alternative reading -- a flow whose only work is inside the function, so `selected` stays 0 -- is disposed of by the same measurement: there the filter is quiet with OR without the declaration, which makes the sentence vacuous rather than correct. Bounded in-place: `flow-function.zod.ts` is one file outside the three-mention surface, carrying the two source sentences that mention 3 mirrors. Fixing the mirror while leaving its source backwards would manufacture exactly the disagreement this card exists to remove, so both are corrected here -- the `FlowFunctionDeclarationSchema` TSDoc and the author-facing `history` string in its unknown-key message. Prose and one error-message string only -- no behaviour, no schema, no accept-set change. `check:generated` reports all 14 artifacts up to date, so nothing regenerated. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LpRNHxWZgSUgVnFT9mQQo4
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 126 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 df9e8d45d55d243b6c2d8de7fb21b13d41e6a9a0 && git checkout df9e8d45d55d243b6c2d8de7fb21b13d41e6a9a0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d48929efe794b467c169825ecb68a2d015f09d8d baab1dda2c99680f0d6b5cb1e29d518a8ddff500 && git checkout -B drift-repro d48929efe794b467c169825ecb68a2d015f09d8d && git merge --no-ff baab1dda2c99680f0d6b5cb1e29d518a8ddff500
node scripts/docs-audit/affected-docs.mjs --json d48929efe794b467c169825ecb68a2d015f09d8d |
…'s flow-function row The row's closing sentence carries two defects measured since it was written: it teaches the broken-sweep predicate as a detector rather than a first filter (#12685), and it states the misspelled-`effect` causal direction backwards (#13096) — a dropped `effect: 'writes'` folds the run to `selected > 0, acted 0, unmeasured 0`, which SATISFIES the filter and lands the run INSIDE the candidate set rather than escaping it. This is a dated audit record, so the sentence is left byte-identical and a dated erratum note is appended beside the row instead (route B), following the `docs/audits/` correction-blockquote convention. Part of #13101 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LpRNHxWZgSUgVnFT9mQQo4
Fixes #13063
The residue outside the fences of #12722 and #12900: three mentions inside
packages/spec/src/automationstill taught the broken-sweep predicate as a detector. Reworded to the shape the sibling surfaces now agree on after #13068 — the three-clause predicate is the FIRST FILTER, not a verdict; the per-node fold (FlowRunSummary.nodes[]/gates[]) is the discriminator; each clause keeps its own true point.The three mentions, before and after
packages/spec/src/automation/execution.zod.ts(theunmeasuredEffectrationale, ships in.d.ts)0fires the broken-sweep alert on a healthy run until operators learn to ignore it, and an overstated1makes the alert never fire at all"0puts a run that DID act inside the broken-sweep FIRST FILTER (selected > 0 AND acted = 0 AND unmeasured = 0), and an overstated1keeps a run that acted on nothing outside it" — plus a new paragraph naming the filter a first filter and not a verdict, and the per-node fold as the discriminatorpackages/spec/src/automation/execution.test.ts(test comment)acted: 0is INCOMPLETE, not zero, and the broken-sweep query has to be able to tell."acted: 0is INCOMPLETE, not zero. The third clause of the broken-sweep FIRST FILTER is what reads that distinction, which is why the tally is carried separately … The filter only selects candidates — a healthy idempotent sweep satisfies it too, and the per-node fold discriminates (#12685)"packages/spec/src/automation/flow-function.test.ts(test comment mirroring a source sentence #13068 reworded)selected > 0, acted 0, unmeasured 0— which SATISFIES #4354's broken-sweep FIRST FILTER. The run lands in the candidate set reading exactly like a dead sweep, on a flow that did its work"Mention 1 was not merely stylistically off. "fires … on a healthy run until operators learn to ignore it" is the exact muting failure #12685 names, stated there as if it were peculiar to an understated
0— when after #12685 it is what the predicate does on every healthy idempotent sweep. The rewrite moves that fact to where it belongs and states what a fabricated count really costs instead: the per-node fold is the step that would have settled it, and a fakedactedis a fact the fold can only repeat.Mentions 2 and 3 are comments; both tests' own assertions are untouched and still pass. Mention 2's test still pins that
unmeasuredis carried distinctly fromacted.The causal direction — resolved by measurement, not by ruling
The card's second half asked whether the "stays silent on the one flow that needed it" sentences state their direction backwards. They do. Read forward in code:
Producer —
packages/services/service-automation/src/builtin/screen-nodes.ts, thescriptexecutor:So the step reports
unmeasuredEffectonly when the registration declared'writes'. A misspelledeffectKEY is dropped at the schema and never looked for bynormalizeFlowFunctionEntry(which reads two keys by construction), so the registration falls back to the'pure'default and the step reports no metrics.Fold —
ExecutionStepMetrics.unmeasuredEffectfolds into the run'sunmeasuredtally onFlowRunSummary. With the declaration lost, that tally is0.Filter — the run therefore reports
selected > 0, acted 0, unmeasured 0, and all three clauses ofselected > 0 AND acted = 0 AND unmeasured = 0are true. The run SATISFIES the filter: it lands INSIDE the candidate set, reading exactly like a dead sweep, on a flow that actually did its work. That is a false positive — not silence.Two independent confirmations already in the tree:
@moduleblock has always stated it this way: "A function that writes anyway makes its run under-report —selected: 30, acted: 0on a run that wrote 30 invoices, which reads exactly like the broken sweep Surface flow run summaries (selected / acted / skipped) — a scheduled flow that does nothing is currently indistinguishable from one with nothing to do #4354 exists to detect". Soflow-function.zod.tsdisagreed with itself about 100 lines apart.packages/qa/dogfood/test/flow-function-effect.dogfood.test.tsasserts both legs end to end through the real HTTP + automation stack. Declared writer:expect(summary.unmeasured).toBe(1)— the filter does not match. Pure:expect(summary.unmeasured).toBe(0), with the test's own comment saying the predicate "is free to fire on this run". Its header states the defect in as many words: "the run still succeeds, it just reportsacted: 0, unmeasured: 0on a run that wrote, which is indistinguishable from a dead sweep".The card's alternative reading is disposed of by the same measurement. The card offered that "the one flow that needed it" might mean a flow whose only work happens inside the function, so
selectedstays 0 and the filter is quiet regardless. But in that sub-case the filter is quiet with or without the declaration, which makes the sentence vacuous rather than correct. There is no reading on which "stays silent" is the measured consequence, so this half is landed rather than escalated.Bounded in-place: one file outside the three-mention surface
packages/spec/src/automation/flow-function.zod.tscarries the two source sentences that mention 3 mirrors (theFlowFunctionDeclarationSchemaTSDoc, and the author-facinghistorystring in its unknown-key message). Correcting the mirror while leaving its source backwards would manufacture exactly the source-vs-mirror disagreement this card exists to remove — the same fence-residue pattern that produced the card. Both are corrected here:flow-function.zod.tsTSDocselected > 0, acted 0, unmeasured 0and lands INSIDE #4354's broken-sweep first filter, reading exactly like the broken sweep that filter exists to detect … Declaringeffect: 'writes'is what would have kept it out (unmeasured > 0); the misspelling is what dropped the declaration."flow-function.zod.tshistorystringselected > 0 AND acted = 0 AND unmeasured = 0) on the very flow whose work the declaration would have accounted for."Conditions checked before taking it: same defect class (the card raises it); mechanical, with the correct shape pinned by the measurement above; no other claim holds the file (#13068 merged 02:48Z, and none of the four sibling
claude/issue-13*branches touches anyautomationpath); same gate family, no new verification surface.Cross-surface agreement
After this change, every live surface in
packages/spec/srcthat names the broken sweep states it the same way — a first filter with the per-node fold as discriminator, and no alarm vocabulary. Bounding scan over the whole tree:15 hits, all in the ruled shape. Residual-alarm and residual-backwards-direction scans both return zero:
packages/spec/src/integration/connector.zod.ts(corrected by #12722) andflow-function.zod.tslines 23/40/73/80/91 (corrected by #13068) already agreed and are untouched.One out-of-scope carrier remains and is deliberately not edited:
docs/audits/2026-07-unknown-key-strictness-ledger.mdline 726 repeats both the old vocabulary and the backwards direction. It is a dated audit record, and silently rewriting a record is a different decision from correcting live contract prose — reported to the PM rather than swept in here.Evidence
Every exit code below was captured before any pipe; gate verdicts are quoted from the line the gate itself prints. The union was re-run after the final commit, at
baab1dda2, with the worktree clean (git status --porcelainempty before and after).Tests (at
baab1dda2, under the shared verify lock):Generated artifacts —
pnpm --filter @objectstack/spec run check:generated, exit 0:Worth recording, because it burned a round: on the unbuilt worktree this same gate reported
api-surface/stale. Afterpnpm --filter @objectstack/spec buildandgen:api-surface, the artifact came back byte-identical — the staleness was the staledist/*.d.ts, not this change. No artifact moves, andcheck:docsis green, so the.mdxundercontent/docs/references/automation/carries only the@moduleblocks and none of the edited prose.Typecheck with
--listFilescoverage proof.packages/spec/tsconfig.jsonexcludes**/*.test.ts, so "typecheck is clean" says nothing about the two edited test files on its own. Both programs measured, counting each edited path in the emitted file list:tsconfig.jsonprogramtsconfig.test.jsonprogramautomation/execution.zod.tsautomation/flow-function.zod.tsautomation/execution.test.tsautomation/flow-function.test.tspnpm --filter @objectstack/spec run typecheckexits 0, and its ratchet leg prints:The 263 are the pre-existing held debt, unchanged, and zero of them fall in any file this PR edits (measured against the raw
tsc -p tsconfig.test.jsonerror list).Gate family — derived from the real change set with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(the script reads its own change set from the merge base; re-derived after the changeset was added). 38 families matched. 34 measured green, includingcheck:authorable-surface,check:docs,check:strictness-ledger,check:liveness,check:empty-state,check:variant-docs,check:skill-refs,check:doc-authoring,check:doc-formula-expressions,check:cross-package-test-inputs,check:published-files,check:merge-driver,check:spec-parsed-alias,check:test-source-alias,check:type-source-resolution,check:nul-bytes, and the convention-triggeredcheck:query-options-erasure,check:type-check-coverage,check:engine-double-contract,check:where-matcher.4 are NOT MEASURED — each exited on an explicit unmet prerequisite before running a single check, so none is a finding, and each needs either a full workspace build or a CI-only input. Declared narrowing; CI owns these:
node scripts/check-dev-prereqs.mjs— "The workspace is not built — 1 unmet precondition, not a list of problems" (66 of 67 packages have nodist/in a fresh worktree).pnpm check:dual-build-cjs-loads— "PREREQUISITE NOT MET — this gate reads built output … ⛔ This is NOT a pass: nothing was measured." Its self-test passed: "37 cases pass".node scripts/check-test-completeness.mjs— takes CI's turbo test log as an argument; standalone it prints only its usage line.node scripts/pm/check-half-states.mjs— "PREREQUISITE NOT MET — the token in the environment is not a valid GitHub credential" (the container's 14-byte placeholder). A PM board sweep, unrelated to this diff.Comment-only proof. Mentions 2 and 3 and the
flow-function.zod.tsTSDoc are comments — no runtime bytes move, shown textually: the diff hunks for those three sites contain only//and*lines, and the two suites' 57 assertions are unchanged and green. The one non-comment byte in this PR is thehistorystring, which is why a changeset is written (#13068 moved TSDoc only and needed none).No ablation is owed. This PR changes prose and one error-message string; there is no guard whose removal could be shown to turn a suite red, and nothing here can fail in a way a mutation would demonstrate.
Generated by Claude Code