Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .changeset/spec-broken-sweep-predicate-is-a-filter.md
Original file line numberDiff line numberDiff line change
@@ -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.
17 changes: 14 additions & 3 deletions packages/spec/src/automation/execution.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -168,7 +168,15 @@ export type FlowRunGateSummary = z.input<typeof FlowRunGateSummarySchema>;
*
* 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.
*
Expand All@@ -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
Expand Down
15 changes: 9 additions & 6 deletions packages/spec/src/integration/connector.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -616,12 +616,15 @@ export type ConnectorStatus = z.input<typeof ConnectorStatusSchema>;
*
* `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`
Expand Down
Loading