From ecdc0d8f928ebfd5c6f8f1431c456c1a147f6752 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 07:17:08 +0000 Subject: [PATCH] docs(spec): state the broken-sweep predicate as the first filter, not 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 Claude-Session: https://claude.ai/code/session_01LpRNHxWZgSUgVnFT9mQQo4 --- ...spec-broken-sweep-predicate-is-a-filter.md | 31 +++++++++++++++++++ packages/spec/src/automation/execution.zod.ts | 17 ++++++++-- .../spec/src/integration/connector.zod.ts | 15 +++++---- 3 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 .changeset/spec-broken-sweep-predicate-is-a-filter.md diff --git a/.changeset/spec-broken-sweep-predicate-is-a-filter.md b/.changeset/spec-broken-sweep-predicate-is-a-filter.md new file mode 100644 index 0000000000..587d7a4d7b --- /dev/null +++ b/.changeset/spec-broken-sweep-predicate-is-a-filter.md @@ -0,0 +1,31 @@ +--- +"@objectstack/spec": patch +--- + +docs(spec): qualify the broken-sweep predicate in `FlowRunSummary` / `ConnectorActionEffect` TSDoc (#12722) + +Text only — no behaviour, no schema, no accept-set change. These doc blocks ship +in `.d.ts`, so a consumer reading the type got the sentence #12685 measurably +disproved. + +`selected > 0 AND acted = 0 AND unmeasured = 0` was stated as *the* broken-sweep +signal / query / alert across three doc blocks in two files. It is the FIRST +FILTER, not a verdict: a healthy idempotent sweep — re-select the same records, +gate each one on "already handled" — satisfies it on every run while that work +stands, so "over N consecutive runs" does not separate it from a dead gate +either (consecutiveness filters flapping, which is a different failure). What +discriminates is the per-node fold: a healthy skip is accounted for by a read +the run performed (`runs > 0` and `selected > 0` in `nodes[]`), while a dead +gate skips just as often with nothing behind it (`runs: 0`, or `selected: 0`). + +The wording mirrors what #12721 landed on `sys_automation_run`'s field +descriptions and `content/docs/automation/flows.mdx`, so the schema doc and the +object description now say the same thing — the point of the card, since the +schema doc is what a platform author actually reads. A corrected sentence in one +surface and a wrong one in the other is the state most likely to produce the +failure #12685 names: an operator wires an alert to the documented predicate, it +fires during normal operation, and it gets muted — leaving a dead sweep +unmonitored while looking monitored. + +The `unmeasured` clause's own point is unchanged, and now explicit: a run with +uncountable effects has an INCOMPLETE `acted` count, not a zero one. diff --git a/packages/spec/src/automation/execution.zod.ts b/packages/spec/src/automation/execution.zod.ts index 08296776c6..f30cc43573 100644 --- a/packages/spec/src/automation/execution.zod.ts +++ b/packages/spec/src/automation/execution.zod.ts @@ -168,7 +168,15 @@ export type FlowRunGateSummary = z.input; * * The counters exist to answer one question no other surface could: is a green * run doing its job, or has it silently stopped? `selected > 0 && acted == 0` - * over consecutive runs is the broken-sweep signal — the platform ships the + * (with `unmeasured = 0`) is the FIRST FILTER for a broken sweep, not a verdict + * (#12685): a healthy idempotent sweep that re-selects the same records and + * gates each one on "already handled" satisfies it on every run while that work + * stands, so "over N consecutive runs" does not separate the two — + * consecutiveness filters flapping, which is a different failure. What + * separates them is the per-node fold below: 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 `nodes[]` — while a dead gate skips just as often with + * nothing behind it (`runs: 0`, or `selected: 0`). The platform ships the * measurement so every flow gets it, rather than each app rebuilding a detector * out of the same primitives that fail silently. * @@ -185,9 +193,12 @@ export const FlowRunSummarySchema = lazySchema(() => z.object({ /** * The qualifier `acted` needs to be trusted. A run that dispatched an * uncountable effect (a `connector_action`) can report `acted: 0` while - * having done plenty, so the broken-sweep query is + * having done plenty, so the broken-sweep FILTER is * `selected > 0 AND acted = 0 AND unmeasured = 0` — the third clause is what - * keeps the alert off healthy connector-driven flows. + * keeps it off healthy connector-driven flows, because such a run has an + * INCOMPLETE `acted` count, not a zero one. A filter, not a verdict: what + * actually separates a broken sweep from a healthy idempotent one is the + * per-node fold, spelled out on `FlowRunSummarySchema` above (#12685). * * Optional, and `undefined` is NOT `0`: a run recorded before this field * existed did not track uncountable effects at all, and defaulting it to zero diff --git a/packages/spec/src/integration/connector.zod.ts b/packages/spec/src/integration/connector.zod.ts index d1593d5089..2d23a42724 100644 --- a/packages/spec/src/integration/connector.zod.ts +++ b/packages/spec/src/integration/connector.zod.ts @@ -616,12 +616,15 @@ export type ConnectorStatus = z.input; * * `connector_action` dispatches to a handler that reaches an external system; * nothing on this side can see what happened there. #4354's per-run summary - * reports `selected` / `acted` and the broken-sweep alert is - * `selected > 0 AND acted = 0 AND unmeasured = 0`, so the two guesses are both - * wrong in a costly direction: a blanket `acted: 0` trips the alert on every - * healthy connector sweep until operators learn to ignore it, and a blanket - * `acted: 1` makes a read-only sweep look busy forever so the alert never - * fires. `http` gets to skip this question because the HTTP method answers it + * reports `selected` / `acted`, and a broken sweep is FIRST FILTERED by + * `selected > 0 AND acted = 0 AND unmeasured = 0` — a filter, not a verdict + * (#12685: a healthy idempotent sweep matches it on every run too, and what + * discriminates is the per-node fold in `FlowRunSummary.nodes[]` / `gates[]`). + * The two guesses are wrong in a costly direction even so: a blanket `acted: 0` + * puts every healthy connector sweep into that candidate set until operators + * learn to ignore it, and a blanket `acted: 1` makes a read-only sweep look + * busy forever so it never becomes a candidate at all. `http` gets to skip this + * question because the HTTP method answers it * (`GET` reads, anything else mutates); a connector action's key does not. * * ## Why the vocabulary differs from `FlowFunctionEffectSchema`