Skip to content

fix(service-automation): the broken-sweep predicate is a first FILTER, not the detector - #12721

Merged
os-litant merged 3 commits into
mainfrom
claude/issue-12685-broken-sweep-predicate
Aug 27, 2026
Merged

fix(service-automation): the broken-sweep predicate is a first FILTER, not the detector#12721
os-litant merged 3 commits into
mainfrom
claude/issue-12685-broken-sweep-predicate

Conversation

@os-litant

@os-litantos-litant commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Fixes#12685

Documentation-contract fix. No counter, no schema, no engine behaviour changes — the run summary is correct and untouched, and hotcrm's flow-run-summary.test.ts pin (both shapes trip the run-level predicate) moves nothing, because the counts it reads are unchanged.

The wrong claim

sys_automation_run.acted_count advertised selected_count > 0 AND acted_count = 0 AND unmeasured_count = 0 as the broken-sweep signal, unqualified. The card's measured A/B shows it cannot separate a healthy idempotent sweep from a dead gate — both fire — and "over N consecutive runs" does not rescue it, because the healthy steady state is persistent rather than transient.

Reproduced here on the platform's own engine (run-summary.test.ts), on one graph pair whose two shapes differ only in where the gate sits relative to the lookup:

runtotalsthe run-level filterthe lookup behind the gategate skips
healthy steady state — every stalled deal already nudgedselected 6, acted 0matchesruns: 3, selected: 33
dead gate — gate in front of the lookup, never opensselected 3, acted 0matchesruns: 03
wrong gate — lookup runs, finds nothing, gate closes anywayselected 3, acted 0matchesruns: 3, selected: 03
genuinely idle — nothing stalledselected 0, acted 0quiet0

What changed

  • acted_count states the predicate as the first filter, says in one sentence why it is not a verdict, and names the discriminator: a healthy skip is accounted for by a read this run performed (the lookup the gate depends on shows runs > 0 and selected > 0 in summary_json.nodes[]), while a dead gate skips just as often with nothing behind it (runs: 0, or selected: 0).
  • skipped_count points at the same fold — gates[] names which edge closed and how often, nodes[] says whether the lookup behind it found anything. Its existing "names the gate as the suspect" line now says what convicts or clears it.
  • unmeasured_count keeps its own point (why the third clause exists) and calls the query a filter rather than an alert.
  • The block comment over the four columns records the measurement so the next reader does not re-derive it.
  • content/docs/automation/flows.mdx carries the same correction for the operator wiring the alert: the measured table above, why consecutiveness does not help, how to read the fold, and the two authoring shapes that decide how good a sweep's signal can be (exclude already-handled records in the query where it can be expressed; otherwise make the "already handled?" check a real lookup node so the run records the evidence). It also states the limit plainly — a gate deciding on a field the run already read leaves no second read to account for its skips, and there the summary cannot separate a correct decision from a stuck one.

The discriminating data was already shipped; nothing new is measured, and no detector is implemented in platform code — the platform documents the rule and ships the data, as before.

The test is a pin on the sentence, not a new predicate

run-summary.test.ts gains an engine-driven pair asserting that both shapes match the run-level filter and that the per-node fold separates them. The rule lives in a local test helper, deliberately: it makes the shipped description executable in the repo that ships it, without giving the platform a detector it does not have. The existing fake data engine learned one more answer (what an idempotency lookup finds) rather than a second double being added beside it.

Verification

Union derived from the actual changed set with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (4 paths; the script reads the merge base itself). Exit codes captured before any pipe; the verdict lines below are the gates' own.

  • pnpm --filter @objectstack/service-automation test91 files, 1091 tests passed.
  • pnpm --filter @objectstack/service-automation exec vitest run src/run-summary.test.ts40 passed.
  • Reverse verification: flipping the dead-gate discriminator assertion to its opposite turns exactly that one test red (1 failed | 39 passed), restored from HEAD with git diff HEAD empty afterwards. The healthy/dead pair is mutually falsifying — a helper stuck at either verdict fails one half.
  • pnpm --filter @objectstack/service-automation exec tsc --noEmit3 errors, the frozen ledger count, all pre-existing TS2341 in nested-region-parity.test.ts; the shrink-only ratchet does not move. --listFiles confirms both edited files are in that program, so the reading covers them.
  • node scripts/check-type-check-coverage.mjsOK — 65/78 workspace packages type-checked (plus the root), 13 in the DEBT ledger, 1 exempt.
  • pnpm lint (repo-wide eslint . --no-inline-config) — green, whole repo, no narrowing claimed.
  • Derived gates, all green: check:changeset-gate-self-tests, check:objectui-changeset, check:pm-half-states, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, release-rehearsal-clone --self-test, check:nul-bytes, check:doc-anchors, check:doc-authoring, check:docs-single-h1, check:docs-redirects, check:docs-audit-scope, check-doc-frontmatter, check-doc-route-spelling, check-docs-section-name, check-section-landing-index, check:role-word, check:published-readme-links, check:react-page-adapter-contract, check:doc-formula-expressions, check:doc-security-posture, spec check:docs / check:empty-state / check:liveness / check:strictness-ledger / check:variant-docs / check:skill-examples, check:engine-double-contract, check:where-matcher, check:query-options-erasure, check:objectql-double-limit, check:cross-package-test-inputs, check-ci-filter-parity, check-comment-mask-adoption, check:page-declaration-shape, check:slot-lookup, check:published-files, check:test-source-alias, check:type-source-resolution, check-plugin-teardown-shape, docs-audit/check-affected-docs, docs-audit/check-drift-comment.
  • check:skill-examples first refused with packages/client-react/dist holds no .d.ts — NOT MEASURED, not red. Built @objectstack/client-react and re-ran: 260 prose examples type-check across 3 surfaces. The four marked blocks on this page are outside the edited region.
  • node scripts/pm/check-half-states.mjs — exit 3, PREREQUISITE NOT MET (no valid GitHub credential in this container). NOT MEASURED, and unrelated to this diff: it sweeps the issue board, not the tree.

Ratchet family re-run on the pushed head 48940d54c (tree identical to the verified 67c01af5f, proven by an empty git diff between them).

Generated by Claude Code

os-litantand others added 3 commits August 27, 2026 12:48
…, not the detector
`sys_automation_run.acted_count` advertised
`selected_count > 0 AND acted_count = 0 AND unmeasured_count = 0` as *the*
broken-sweep signal, unqualified. Measured A/B on one graph pair through the
real engine: a healthy idempotent sweep (re-select the same records, gate each
one on "was this already handled") and a dead gate both report
`selected > 0, acted 0, unmeasured 0`. The predicate cannot make the one
distinction it was advertised to make, and "over N consecutive runs" does not
rescue it — the healthy steady state is persistent, so it trips on every run.
Documentation-contract fix only: no counter, no schema and no engine behaviour
changes. The descriptions now state the predicate as a first filter and name
the discriminator that was already shipped in the per-node `summary_json` fold —
a healthy skip is accounted for by a read the run performed, a dead gate skips
just as often with nothing behind it. `run-summary.test.ts` pins the pair as
executable evidence; the detector itself stays the operator's, not the
platform's.
Co-authored-by: Claude <noreply@anthropic.com>
@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, so this run has nothing to listnot 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
  • 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 — 5 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 4af6c4419d7669a56940ec8e7adbb133829fcbdepackageMentionDocs.

Which tree this was computed on

This run read content/docs from cde974a392c300a282ffced0b2d61aede2b13113 — the merge of head 48940d54c74c39da9bf796416b7651a2a1f8bb08 into base 4af6c4419d7669a56940ec8e7adbb133829fcbde, 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 cde974a392c300a282ffced0b2d61aede2b13113 && git checkout cde974a392c300a282ffced0b2d61aede2b13113
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4af6c4419d7669a56940ec8e7adbb133829fcbde 48940d54c74c39da9bf796416b7651a2a1f8bb08 && git checkout -B drift-repro 4af6c4419d7669a56940ec8e7adbb133829fcbde && git merge --no-ff 48940d54c74c39da9bf796416b7651a2a1f8bb08
node scripts/docs-audit/affected-docs.mjs --json 4af6c4419d7669a56940ec8e7adbb133829fcbde

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

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 27, 2026
@os-litant
os-litant marked this pull request as ready for review August 27, 2026 14:23
@os-litant
os-litant enabled auto-merge August 27, 2026 14:23
@os-litant
os-litant added this pull request to the merge queueAug 27, 2026
Merged via the queue into main with commit 8bb05eaAug 27, 2026
40 of 41 checks passed
@os-litant
os-litant deleted the claude/issue-12685-broken-sweep-predicate branch August 27, 2026 15:06
os-trump pushed a commit that referenced this pull request Aug 28, 2026
… the detector (#12722)
The three-clause query `selected > 0 AND acted = 0 AND unmeasured = 0` was
stated as *the* broken-sweep signal / query / alert across three doc blocks in
two files. #12685 measured that it cannot separate a healthy idempotent sweep
from a dead gate -- both match, on every run -- so "over N consecutive runs"
does not rescue it either.
Reword to mirror what #12721 landed on `sys_automation_run`'s field
descriptions and content/docs/automation/flows.mdx: the query is the FIRST
FILTER, and the per-node fold (`nodes[]` / `gates[]`) is what discriminates.
These blocks ship in .d.ts, so the schema doc a platform author reads now
agrees with the object description.
TSDoc text only -- no behaviour, no schema, no accept-set change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LpRNHxWZgSUgVnFT9mQQo4
os-trump pushed a commit that referenced this pull request Aug 29, 2026
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

1 participant

@os-litant