Uh oh!
There was an error while loading. Please reload this page.
fix(service-automation): the broken-sweep predicate is a first FILTER, not the detector - #12721
Conversation
…, 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>
📓 Docs Drift Check1 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 — 5 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 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 |
Uh oh!
There was an error while loading. Please reload this page.
… 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
…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
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.tspin (both shapes trip the run-level predicate) moves nothing, because the counts it reads are unchanged.The wrong claim
sys_automation_run.acted_countadvertisedselected_count > 0 AND acted_count = 0 AND unmeasured_count = 0as 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:runs: 3,selected: 3runs: 0runs: 3,selected: 0What changed
acted_countstates 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 showsruns > 0andselected > 0insummary_json.nodes[]), while a dead gate skips just as often with nothing behind it (runs: 0, orselected: 0).skipped_countpoints 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_countkeeps its own point (why the third clause exists) and calls the query a filter rather than an alert.content/docs/automation/flows.mdxcarries 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.tsgains 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 test— 91 files, 1091 tests passed.pnpm --filter @objectstack/service-automation exec vitest run src/run-summary.test.ts— 40 passed.HEADwithgit diff HEADempty afterwards. The healthy/dead pair is mutually falsifying — a helper stuck at either verdict fails one half.pnpm --filter @objectstack/service-automation exec tsc --noEmit— 3 errors, the frozen ledger count, all pre-existingTS2341innested-region-parity.test.ts; the shrink-only ratchet does not move.--listFilesconfirms both edited files are in that program, so the reading covers them.node scripts/check-type-check-coverage.mjs—OK — 65/78 workspace packages type-checked (plus the root), 13 in the DEBT ledger, 1 exempt.pnpm lint(repo-wideeslint . --no-inline-config) — green, whole repo, no narrowing claimed.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, speccheck: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-examplesfirst refused withpackages/client-react/dist holds no .d.ts— NOT MEASURED, not red. Built@objectstack/client-reactand 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 verified67c01af5f, proven by an emptygit diffbetween them).Generated by Claude Code