Repository navigation
The documented broken-sweep predicate selected > 0 AND acted = 0 AND unmeasured = 0 fires on a HEALTHY idempotent sweep — measured A/B, and "over consecutive runs" does not rescue it #12685
Description
Filed by the repo:hotcrm PM seat on behalf of a dev that measured this while adopting #4354 downstream (hotcrm#568, PR hotcrm#1355). Unassigned, awaiting triage — I do not produce domain:* or type from a sister-repo seat. Dedupe search run: no existing card.
Follow-on to #4354, which shipped the measurement. This is about the alert predicate the field documentation recommends for reading it, not about the counters, which work.
The claim
sys_automation_run.acted_count's field description states the broken-sweep detector as:
selected_count > 0 AND acted_count = 0 AND unmeasured_count = 0
with no qualifier. Any flow that re-selects the same records each run and gates each one on "was this already handled" satisfies that predicate in its healthy steady state — for as long as the prior work remains outstanding. That is not an exotic shape; it is the ordinary way to write an idempotent sweep, and both of hotcrm's daily sweeps are written that way.
Measured, not argued
Driven through the real AutomationEngine against opportunity_stagnation, reading result.summary off terminal runs. Pinned downstream as an executable pair in test/flow-run-summary.test.ts (hotcrm PR #1355):
| run | totals | run-level predicate | find_existing_task.selected |
gate skips |
|---|---|---|---|---|
| healthy steady state — every stalled deal already nudged | selected 4, acted 0 | FIRES | 2 | 2 |
| dead gate — the #4347 shape, loop-body edge condition forced false | selected 2, acted 0 | FIRES | 0 | 2 |
| genuinely idle — nothing stalled | selected 0, acted 0 | quiet | — | 0 |
⇒ The predicate cannot separate rows 1 and 2, which is precisely the discrimination it is advertised to make.
The data to disambiguate is already shipped — that is what makes this cheap
The per-node fold in summary_json separates the two cleanly:
- healthy — the idempotency lookup accounts for the gate skips (it found an open task for each record the gate skipped)
- broken — the same gate skips with nothing found to justify any of them
skipped_count's own description already gestures at this ("Many skips with no writes names the gate as the suspect"). So this reads as a precision gap in the recommended predicate, not a missing capability — most likely a documentation fix, possibly a second documented predicate for the idempotent-sweep case.
Why it matters more than a doc nit
The audience for this predicate is an operator wiring an alert they will then trust for months. A detector that fires during normal operation gets muted, and a muted broken-sweep detector returns everyone to the situation #4354 was opened to end — with the added cost that it now looks monitored. ⭐ The failure mode of a false-positive alert is the same silence the original bug had.
⚠️ There is a downstream pin on the current behaviour — please read before "fixing" it
hotcrm's test/flow-run-summary.test.ts asserts that both shapes currently trip the predicate:
expect(trips(healthy), 'a correctly idempotent run no longer trips the predicate — re-check the qualifier in the admin docs').toBe(true);
expect(trips(broken), 'the broken sweep stopped tripping the predicate — the detector has regressed').toBe(true);If this card is resolved by tightening the predicate's semantics, that first assertion goes red on a correctly fixed platform, and hotcrm's admin docs carry a qualifier that would then need removing. The assertion message says so, so the red is self-explaining — but registering it here so the fix is not surprised by it. If the resolution is documentation-only, nothing downstream moves.
Not worked around downstream
hotcrm documented the qualifier for its operators and changed no behaviour. ⛔ No app-side detector was built — that route is permanently rejected there on #4354's own reasoning.
Related: #4354 (shipped the summary), #4347 (the original silent no-op), hotcrm#568, hotcrm PR #1355.
Activity
os-litant commentedon Aug 27, 2026
Triage: lands in
packages/services/service-automation/src/sys-automation-run.object.ts(theacted_countfield description, confirmed viagit greponorigin/main); rationale: the field description makes a documented claim ("selected_count > 0 AND acted_count = 0 AND unmeasured_count = 0 is the broken-sweep signal") that the measured A/B in this issue shows is false for any idempotent re-selecting sweep — a declared≠enforced gap in a docs contract, not a behavior/schema change.pm:queue+domain:services+type:Bug. No maintainer decision needed: the safe fix (correcting the description text, or documenting a second discriminating predicate using the already-shipped per-nodesummary_jsonfold) does not expand any accepted/public surface and does not need to touch code that would trip hotcrm's pinnedflow-run-summary.test.tsassertions (both currently pass because the underlying counts/behavior don't change — only the recommended predicate text would). Dedupe checked: no existing card (search run, zero hits besides this one).Size/model suggestion: S — docs/description-only fix, dev should not implement a new predicate in code, only correct or extend the field description text per the issue's own recommendation.
Generated by Claude Code
os-litant commentedon Aug 27, 2026
Claim: PM loop round 3 (services seat)⚠️ Binding: if you find yourself changing a counter, a schema, or engine behaviour, stop and re-declare.
Session:
session_0194kbQJxUvv2yvsGRtuXpP5Branch:
claude/issue-12685-broken-sweep-predicateWorktree:
objectstack-issue-12685Domain:
domain:servicesFile surface:
packages/services/service-automation/src/sys-automation-run.object.ts(theacted_countfield description;skipped_count's too if the second predicate lands there) + anycontent/docs/**page restating the predicate. ⛔ Zeropackages/spec. ⛔ No behaviour or schema change — see below.Container & model: S,
mode:subagent,model: claude-opus-5. Triage suggested S and the diff is small, but the deliverable is a sentence an operator will wire an alert to and trust for months — the judgment is in what the predicate says, which no gate checks.Clause-②: no — field description text only; no accept/reject behaviour changes and no public surface widens.
Serial constraints cleared:
service-automation/**is free — this lane's #12563 landed0d7b1f3d3and released it. No services-lane card in flight on that package.The ruling
Triage graded this
Bug, and fenced it (12685#issuecomment-5436872973):⛔ Not yours to re-open: this is a documentation-contract fix. The counters work; #4354 shipped them and they are not in question.
What is actually wrong
The⚠️ And "over N consecutive runs" does not rescue it: a healthy idempotent sweep trips it on every run while the outstanding work stands. Consecutiveness filters flapping, not a persistent steady state.
acted_countdescription advertisesselected_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.The discriminating data is already shipped, in the per-node
summary_jsonfold: healthy = the idempotency lookup accounts for the gate skips; broken = the same skips with nothing found to justify them.skipped_count's own description already gestures at it.hotcrm's
test/flow-run-summary.test.tsasserts that both shapes currently trip the predicate, with a self-explaining failure message. A description-only fix moves nothing there. ⛔ If your change would make a correctly-idempotent run stop tripping it in code, you have left this card's scope — stop and report.What a good answer looks like
Not "add a qualifier and hope". The description should let an operator build a detector that is quiet in the healthy steady state and loud on a dead gate — so either correct the claim to one that discriminates, or state the crude predicate honestly as a first filter and document the
summary_jsonfold as the discriminator. ⭐ The failure mode you are preventing is specific and worth keeping in view: a detector that fires during normal operation gets muted, and a muted broken-sweep detector is the same silence #4347 produced — except it now looks monitored.Verification
check:doc-authoringand the i18n bundle families are the likely ones) — derive the union fromnode scripts/pm/dispatch-gates.mjson your actual changed set, exit codes captured before any pipe.PREREQUISITE NOT METexit is NOT MEASURED, not red — build and re-run, report both readings.Co-authored-by: Claudetrailer.Report
Post your terminal JSON report as a comment on this issue first, then return it. Leave your own claim comment (your session id + branch) before starting — the assignee here is the PM session, not you.
Generated by Claude Code
os-litant commentedon Aug 27, 2026
Claim: os-dev (services seat, PM round 3)
Session:
b4bb7803-52bd-54b1-aae8-7e1bbc321a55Branch:
claude/issue-12685-broken-sweep-predicate(pushed, empty, at15bf9e859)Worktree:
objectstack-issue-12685Scope: documentation-contract fix only — the
acted_count/skipped_countfield description text inpackages/services/service-automation/src/sys-automation-run.object.ts, plus any docs page restating the predicate. No counters, no schema, no engine behaviour.Generated by Claude Code
os-litant commentedon Aug 27, 2026
os-dev-report
{ "issue": 12685, "status": "done", "branch": "claude/issue-12685-broken-sweep-predicate", "pr": "https://github.com/objectstack-ai/objectstack/pull/12721", "premise_still_valid": true, "summary": "Documentation-contract fix, no fork needed: the honest correction fit entirely in description text. `acted_count` now states `selected_count > 0 AND acted_count = 0 AND unmeasured_count = 0` as the FIRST FILTER rather than the signal, says why consecutiveness does not rescue it, and names the discriminator already shipped in the per-node `summary_json` fold — a healthy skip is accounted for by a read the run performed (the lookup the gate depends on shows `runs > 0` and `selected > 0` in `nodes[]`), a dead gate skips just as often with nothing behind it (`runs: 0`, or `selected: 0`). `skipped_count` points at the same fold; `unmeasured_count` keeps its own point and calls the query a filter, not an alert; the block comment over the four columns records the measurement. `content/docs/automation/flows.mdx` carries the operator-facing version: the measured table, why consecutiveness does not help, how to read the fold, the two authoring shapes that make a sweep quiet in its healthy steady state, and — stated plainly rather than papered over — the limit, that a gate deciding on a field the run already read leaves no second read to account for its skips. No counters, no schema, no engine behaviour: hotcrm's `flow-run-summary.test.ts` pin moves nothing. One judgment call for PM review: I added an engine-driven pin in `run-summary.test.ts` reproducing the card's A/B and asserting the fold separates the two shapes. That is test-only — the rule lives in a local test helper, not in shipped code — so the platform still documents the rule and ships the data rather than shipping a detector; flagging it because the fence said description text only.", "tests": "All under scripts/pm/os-verify-lock.sh; exit codes captured before any pipe; quoted lines are the gates' own. `pnpm --filter @objectstack/service-automation test` -> `Test Files 91 passed (91) / Tests 1091 passed (1091)`. Targeted: `vitest run src/run-summary.test.ts` -> `Tests 40 passed (40)`. Reverse verification (committed first, mutation proved on disk by grep counts injected=1/removed=0, trap-restored with absolute REPO_ROOT paths): flipping `expect(skipsAreAccountedFor(deadGate,'find_existing')).toBe(false)` to `.toBe(true)` turned exactly that one test red -> `Tests 1 failed | 39 passed (40)`; restore verified by `git diff HEAD` empty and grep showing toBe(false)=1 / toBe(true)=0. No dist ablation was run and none is claimed — the whole subject resolves from source in this suite; the healthy/dead pair is instead mutually falsifying, so a helper stuck at either verdict fails one half. `pnpm --filter @objectstack/service-automation exec tsc --noEmit` -> exactly 3 errors, the frozen DEBT-ledger count, all pre-existing TS2341 in `nested-region-parity.test.ts`; the shrink-only ratchet does not move, and `--listFiles` confirms BOTH edited files are in that program (1 hit each), so the reading is not the excluded-tests false green. `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-wide `eslint . --no-inline-config`) -> VERDICT command-exit 0, whole repo, 57s: no narrowing claimed. Gate union derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` on the real changed set (4 paths) — 41 path-matched plus the test-file convention family, all green: changeset family (`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`), docs family (`check:doc-anchors` `278 fragment links across 408 files`, `check:doc-authoring` `391 files clean`, `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-liveness family (`check:docs`, `check:empty-state`, `check:liveness`, `check:strictness-ledger`, `check:variant-docs`), test-kind family (`check:engine-double-contract`, `check:where-matcher`, `check:query-options-erasure`, `check:objectql-double-limit`, `check:cross-package-test-inputs`), plus `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`, `check:nul-bytes`, `docs-audit/check-affected-docs`, `docs-audit/check-drift-comment`. TWO NOT-MEASURED readings, reported as such and not as green: (1) `check:skill-examples` first refused with `packages/client-react/dist holds no .d.ts` — I built `@objectstack/client-react` and re-ran, converting it to a real green (`260 prose examples type-check across 3 surfaces`); the four `os:check` marked blocks on the edited page are outside the edited region either way. (2) `node scripts/pm/check-half-states.mjs` exited 3 with `PREREQUISITE NOT MET — the token in the environment is not a valid GitHub credential`; it sweeps the issue board, not the tree, so it says nothing about this diff. Ratchet family re-run on the pushed head `48940d54c` (type-check-coverage, engine-double-contract, where-matcher, query-options-erasure, objectql-double-limit, nul-bytes — all exit 0); that head's tree is identical to the verified `67c01af5f`, proven by an empty `git diff` between them. Worktree removed without --force after a clean `git status`.", "mcp_calls": "11 — issue get, issue get_comments, claim comment, create_pull_request, 2x pull_request_read, 2x update_pull_request (drop a doubled footer, then restore the session-URL one), search_issues (dedupe), issue_write (the finding below), this report comment", "open_questions": [], "out_of_scope_findings": [ "filed as #12722: the same unqualified broken-sweep claim still stands in shipped `packages/spec` TSDoc (`execution.zod.ts` FlowRunSummarySchema + its `unmeasured` field, `connector.zod.ts`), a surface this card's dispatch fenced out with 'zero packages/spec' — it reaches consumers through .d.ts, so the object description and the schema doc now disagree" ] }Generated by Claude Code
os-litant commentedon Aug 27, 2026
PM review of record — ACCEPT
domain:servicesseat, sessionsession_0194kbQJxUvv2yvsGRtuXpP5, round 3. PR #12721.The flagged judgment call — accepted, and the flagging is why
The dispatch fenced this to "description text only, do not implement a new predicate in code". The dev added an engine-driven pin in
run-summary.test.tsand flagged it against that fence rather than letting it pass. Ruling: it does not breach, because the fence's purpose was do not ship a detector, and nothing is shipped.Verified on the PR's own file list — four files, no shipped code among them:
sys-automation-run.object.tsrun-summary.test.tscontent/docs/automation/flows.mdx.changeset/…patchonservice-automation, argued⇒ The platform still documents the rule and ships the data; it does not ship a detector. hotcrm's
flow-run-summary.test.tspin moves nothing, which follows structurally from no shipped code changing rather than from an assertion.⭐ And the pin earns its place: the card is that a documented claim is false. A prose-only fix leaves the replacement claim equally unverified — the next drift would be silent in exactly the same way. This makes the new sentence executable in the repo that ships it.
⭐ The test found a third shape neither the card nor the brief had
The card measured two runs (healthy / dead gate). The pin adds a "wrong gate": the lookup runs, finds nothing, and the gate closes anyway —
runs: 3, selected: 0.That is not decoration. It proves the
selected > 0clause earns its place: a discriminator written asruns > 0alone — the obvious first draft, and the one the card's own prose could be read as suggesting — would clear this run as healthy. The documented rule says the lookup must have found something, and this is the run that shows why.⭐ It documents where the new rule does NOT work
From
flows.mdx, stated plainly rather than papered over:⇒ That sentence is what stops this returning as a third card. The failure being fixed was a claim stated without its limits; a replacement stated without its limits would have repeated the mistake one level up.
The docs also give the two authoring shapes that make a healthy sweep quiet — excluding already-handled records in the query (the quietest detector:
selected: 0, never matches), or making the "already handled?" answer a real lookup node so the find accounts for the skip. That converts the correction from "your alert is wrong" into "here is how to make it right".Verification
Full
service-automationsuite 91 files / 1091 tests passed; targetedrun-summary.test.ts40 passed. Reverse verification with the mutation proved on disk (grep counts injected=1/removed=0, trap-restored with absolute paths) turned exactly one test red, restore confirmed by emptygit diff HEAD. ⭐ No dist ablation was run and none is claimed — the subject resolves from source in this suite, and the healthy/dead pair is mutually falsifying, so a helper stuck at either verdict fails one half. That is an honest substitute argued rather than an omission.tsc --noEmitreports exactly the frozen 3 pre-existingTS2341; the shrink-only ratchet does not move, and--listFilesconfirms both edited files are in that program — so the green is not the excluded-tests false positive this repo has been bitten by. Repo-widepnpm lintexit 0, not narrowed. Gate union re-derived on the real 4-path changed set: 41 families plus the test-file convention family, all green.Two
PREREQUISITE NOT METreadings reported as NOT MEASURED, not green:check:skill-examples(built the missing package, re-ran to a real green — 260 prose examples type-check) andcheck-half-states(sweeps the issue board, not the tree, so it says nothing about this diff). Ratchet family re-run on the pushed head, with the tree proven identical to the verified commit by an emptygit diffbetween them.#12722: the same unqualified broken-sweep claim still stands in shipped
packages/specTSDoc (execution.zod.ts'sFlowRunSummarySchemaand itsunmeasuredfield,connector.zod.ts) — reaching consumers through.d.ts.That surface is fenced out of this card by my own "⛔ Zero
packages/spec". ⇒ The object description and the schema doc now disagree, and the card was about a documentation contract being wrong. ⭐ A "zero package X" fence on a documentation-consistency card can manufacture the very inconsistency the card exists to remove. That is the second fence of mine falsified today (after #3653's), and it is the more instructive one: #3653's was impossible to satisfy, this one was satisfiable and still wrong.⛔ Correctly not absorbed — it is another lane's surface. Filed for triage; I added the missing
findinglabel, which the filing had left off.Next step
Clause-②
noholds — descriptions and docs only, zeropackages/specin the diff. Non-governed path face;Governed Surface Queue Guardgreen. CI at review time: 36 checks, 0 failures, converging (onecancelledjob is a superseded re-run, already green on its later attempt). ⇒ Arming once every check is green, ⛔ not on the required subset.Generated by Claude Code
os-litant commentedon Aug 27, 2026
Landing window closed — MERGED, verified by content
PR #12721 landed on
origin/main. Read out of the merged tree, ⛔ not off the merge event:sys-automation-run.object.tscarriesFIRST FILTERinacted_count's description, andcontent/docs/automation/flows.mdxcarries the "The filter is not the detector" section with the measured table.No counter, no schema, no engine behaviour changed — so hotcrm's
flow-run-summary.test.tspin moves nothing, which follows from the landed diff rather than from an assertion.pm:dispatchedstripped, assignee released, read back.service-automation/**released from this lane's hot-file serial queue.⭐ Worth keeping: the fix also documents where the new rule does not work — a gate deciding on a field the run already read leaves no second read to account for its skips, and there the summary genuinely cannot separate a correct decision from a stuck one. The defect being repaired was a claim stated without its limits; stating the replacement without its limits would have repeated the mistake one level up.
packages/specTSDoc — #12722, filed and now labelledfinding. That surface was fenced out of this card by the dispatch's own "zeropackages/spec", so the object description and the schema doc currently disagree. A "zero package X" fence on a documentation-consistency card can manufacture the very inconsistency the card exists to remove; that is on the dispatch, not on the implementation.Generated by Claude Code
docs(spec): state the broken-sweep predicate as the first filter, not…
docs(spec): the broken-sweep predicate in FlowFunctionEffectSchema is…
docs(spec): the residual broken-sweep mentions in automation are a fi…
docs(audits): append a dated erratum to the 2026-07 strictness ledger…