Skip to content

docs(spec): the residual broken-sweep mentions in automation are a first FILTER, and the misspelled-effect sentences state the measured direction - #13096

Merged
os-trump merged 1 commit into
mainfrom
claude/issue-13063-broken-sweep-residue
Aug 29, 2026
Merged

docs(spec): the residual broken-sweep mentions in automation are a first FILTER, and the misspelled-effect sentences state the measured direction#13096
os-trump merged 1 commit into
mainfrom
claude/issue-13063-broken-sweep-residue

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes #13063

The residue outside the fences of #12722 and #12900: three mentions inside packages/spec/src/automation still 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

# Site Before After
1 packages/spec/src/automation/execution.zod.ts (the unmeasuredEffect rationale, ships in .d.ts) "an understated 0 fires the broken-sweep alert on a healthy run until operators learn to ignore it, and an overstated 1 makes the alert never fire at all" "an understated 0 puts a run that DID act inside the broken-sweep FIRST FILTER (selected > 0 AND acted = 0 AND unmeasured = 0), and an overstated 1 keeps 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 discriminator
2 packages/spec/src/automation/execution.test.ts (test comment) "acted: 0 is INCOMPLETE, not zero, and the broken-sweep query has to be able to tell." "acted: 0 is 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)"
3 packages/spec/src/automation/flow-function.test.ts (test comment mirroring a source sentence #13068 reworded) "keeping #4354's broken-sweep query silent on the one run that needed it." "the run reports 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 faked acted is 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 unmeasured is carried distinctly from acted.

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:

Producerpackages/services/service-automation/src/builtin/screen-nodes.ts, the script executor:

const unmeasured = registration.effect === 'writes';
…
return {
  success: true,
  output: { function: target, result },
  ...(unmeasured ? { metrics: { unmeasuredEffect: true } } : {}),
};

So the step reports unmeasuredEffect only when the registration declared 'writes'. A misspelled effect KEY is dropped at the schema and never looked for by normalizeFlowFunctionEntry (which reads two keys by construction), so the registration falls back to the 'pure' default and the step reports no metrics.

FoldExecutionStepMetrics.unmeasuredEffect folds into the run's unmeasured tally on FlowRunSummary. With the declaration lost, that tally is 0.

Filter — the run therefore reports selected > 0, acted 0, unmeasured 0, and all three clauses of selected > 0 AND acted = 0 AND unmeasured = 0 are 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:

  1. The same file's @module block has always stated it this way: "A function that writes anyway makes its run under-report — selected: 30, acted: 0 on 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". So flow-function.zod.ts disagreed with itself about 100 lines apart.
  2. packages/qa/dogfood/test/flow-function-effect.dogfood.test.ts asserts 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 reports acted: 0, unmeasured: 0 on 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 selected stays 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.ts carries the two source sentences that mention 3 mirrors (the FlowFunctionDeclarationSchema TSDoc, and the author-facing history string 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:

Site Before After
flow-function.zod.ts TSDoc "so #4354's broken-sweep filter stays silent on the one flow that needed it" "so the run reports selected > 0, acted 0, unmeasured 0 and lands INSIDE #4354's broken-sweep first filter, reading exactly like the broken sweep that filter exists to detect … Declaring effect: 'writes' is what would have kept it out (unmeasured > 0); the misspelling is what dropped the declaration."
flow-function.zod.ts history string "which is what keeps the broken-sweep filter quiet on the run that needed it." "which is what drops the run INTO the broken-sweep first filter (selected > 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 any automation path); same gate family, no new verification surface.

Cross-surface agreement

After this change, every live surface in packages/spec/src that 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:

git grep -n "broken.sweep\|broken sweep" -- packages/spec/src

15 hits, all in the ruled shape. Residual-alarm and residual-backwards-direction scans both return zero:

git grep "broken-sweep alert\|broken-sweep query\|makes the alert never fire" -- packages/spec/src        (0)
git grep "stays silent on the one flow\|quiet on the run that needed it\|silent on the one run that needed it" -- packages/spec/src   (0)

packages/spec/src/integration/connector.zod.ts (corrected by #12722) and flow-function.zod.ts lines 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.md line 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 --porcelain empty before and after).

Tests (at baab1dda2, under the shared verify lock):

pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 \
  src/automation/execution.test.ts src/automation/flow-function.test.ts
  Test Files  2 passed (2)
       Tests  57 passed (57)
os-verify-lock: VERDICT command-exit 0

Generated artifactspnpm --filter @objectstack/spec run check:generated, exit 0:

✓ All 14 generated artifacts are up to date.

Worth recording, because it burned a round: on the unbuilt worktree this same gate reported api-surface/ stale. After pnpm --filter @objectstack/spec build and gen:api-surface, the artifact came back byte-identical — the staleness was the stale dist/*.d.ts, not this change. No artifact moves, and check:docs is green, so the .mdx under content/docs/references/automation/ carries only the @module blocks and none of the edited prose.

Typecheck with --listFiles coverage proof. packages/spec/tsconfig.json excludes **/*.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:

File in tsconfig.json program in tsconfig.test.json program
automation/execution.zod.ts 1 1
automation/flow-function.zod.ts 1 1
automation/execution.test.ts 0 1
automation/flow-function.test.ts 0 1

pnpm --filter @objectstack/spec run typecheck exits 0, and its ratchet leg prints:

check:test-typecheck: OK — @objectstack/spec's test layer compiles under
packages/spec/tsconfig.test.json; 55 file(s) / 263 error(s) held in
test-typecheck-debt.json (shrink-only, #5286)

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.json error 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, including check: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-triggered check: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 no dist/ 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.ts TSDoc 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 the history string, 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

…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
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tests tooling labels Aug 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/spec/src/automation/execution.zod.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/src/automation/execution.zod.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 126 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 d48929efe794b467c169825ecb68a2d015f09d8dpackageMentionDocs.

Which tree this was computed on

This run read content/docs from df9e8d45d55d243b6c2d8de7fb21b13d41e6a9a0 — the merge of head baab1dda2c99680f0d6b5cb1e29d518a8ddff500 into base d48929efe794b467c169825ecb68a2d015f09d8d, 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 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

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

@os-trump os-trump added needs:contract-review and removed documentation Improvements or additions to documentation tests tooling labels Aug 29, 2026 — with Claude
@os-trump
os-trump marked this pull request as ready for review August 29, 2026 05:34
@os-trump
os-trump enabled auto-merge August 29, 2026 05:34
@os-trump
os-trump added this pull request to the merge queue Aug 29, 2026
Merged via the queue into main with commit 8a483b3 Aug 29, 2026
47 checks passed
@os-trump
os-trump deleted the claude/issue-13063-broken-sweep-residue branch August 29, 2026 05:57
os-trump pushed a commit that referenced this pull request Aug 29, 2026
…'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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants