From baab1dda2c99680f0d6b5cb1e29d518a8ddff500 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 04:54:28 +0000 Subject: [PATCH] docs(spec): the residual broken-sweep mentions in automation are a first 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 Claude-Session: https://claude.ai/code/session_01LpRNHxWZgSUgVnFT9mQQo4 --- .../broken-sweep-residue-first-filter.md | 35 +++++++++++++++++++ .../spec/src/automation/execution.test.ts | 9 +++-- packages/spec/src/automation/execution.zod.ts | 17 ++++++--- .../spec/src/automation/flow-function.test.ts | 6 +++- .../spec/src/automation/flow-function.zod.ts | 11 ++++-- 5 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 .changeset/broken-sweep-residue-first-filter.md diff --git a/.changeset/broken-sweep-residue-first-filter.md b/.changeset/broken-sweep-residue-first-filter.md new file mode 100644 index 0000000000..1c37193e0c --- /dev/null +++ b/.changeset/broken-sweep-residue-first-filter.md @@ -0,0 +1,35 @@ +--- +"@objectstack/spec": patch +--- + +docs(spec): the last three broken-sweep mentions in `automation` become the first FILTER, and the misspelled-`effect` sentences state the direction the code measures (#13063) + +#12685 measured that `selected > 0 AND acted = 0 AND unmeasured = 0` cannot separate a +healthy idempotent sweep from a dead gate, and #12721 / #12722 / #12900 / #13068 rewrote +the surfaces that stated it as a detector. Each of those changes was fenced to the doc +blocks its dispatch named, so three mentions inside `packages/spec/src/automation` +survived — including one that 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. + +- `ExecutionStepMetricsSchema`'s `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 — so the block now states what a fabricated + count actually 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. + +Separately, and measured rather than ruled: the two sentences describing a misspelled +`effect` key stated their consequence backwards. Read forward, a lost `effect: 'writes'` +declaration means the `script` step reports no `unmeasuredEffect`, so the run reports +`selected > 0, acted 0, unmeasured 0` — which SATISFIES the filter. The run therefore +lands INSIDE the candidate set reading exactly like a dead sweep, rather than escaping +it; the declaration is what would have kept it out. That is how the same file's `@module` +block already stated it, and what `flow-function-effect.dogfood.test.ts` asserts end to +end. The `FlowFunctionDeclarationSchema` TSDoc and the author-facing `history` string in +its unknown-key message now say so. + +TSDoc and one error-message string only — no behaviour, no schema, no accept-set change. diff --git a/packages/spec/src/automation/execution.test.ts b/packages/spec/src/automation/execution.test.ts index c24d8cecf5..2aae600120 100644 --- a/packages/spec/src/automation/execution.test.ts +++ b/packages/spec/src/automation/execution.test.ts @@ -208,8 +208,13 @@ describe('FlowRunSummarySchema', () => { }); it('carries an uncountable-effect tally distinct from acted', () => { - // A connector-driven run: `acted: 0` is INCOMPLETE, not zero, and the - // broken-sweep query has to be able to tell. + // A connector-driven run: `acted: 0` is INCOMPLETE, not zero. The third + // clause of the broken-sweep FIRST FILTER + // (`selected > 0 AND acted = 0 AND unmeasured = 0`) is what reads that + // distinction, which is why the tally is carried separately instead of + // folded into `acted`. The filter only selects candidates — a healthy + // idempotent sweep satisfies it too, and the per-node fold discriminates + // (#12685) — but merging the two would corrupt even the selection. const summary = FlowRunSummarySchema.parse({ selected: 9, acted: 0, skipped: 0, unmeasured: 3, nodes: [{ nodeId: 'push', nodeType: 'connector_action', status: 'success' as const, runs: 3, failures: 0, skipped: 0, unmeasured: 3 }], diff --git a/packages/spec/src/automation/execution.zod.ts b/packages/spec/src/automation/execution.zod.ts index f30cc43573..a15a5ab7ee 100644 --- a/packages/spec/src/automation/execution.zod.ts +++ b/packages/spec/src/automation/execution.zod.ts @@ -55,10 +55,19 @@ export type ExecutionStatus = z.input; * And `unmeasuredEffect` is the third answer, which a two-counter model would * have had to fake: a `connector_action` reaches an external system, and when * the action declares nothing about whether it reads or writes, `0` understates - * a write and `1` overstates a read. Both are worse than saying so — an - * understated `0` fires the broken-sweep alert on a healthy run until operators - * learn to ignore it, and an overstated `1` makes the alert never fire at all, - * which is the original bug back again. + * a write and `1` overstates a read. Both are worse than saying so, and in + * opposite directions — an understated `0` puts a run that DID act inside the + * broken-sweep FIRST FILTER (`selected > 0 AND acted = 0 AND unmeasured = 0`), + * and an overstated `1` keeps a run that acted on nothing outside it, which is + * the original bug back again. + * + * Being inside that filter is not the accusation an alarm reading makes of it: + * it is a first filter and 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 too, and what separates the two is the per-node + * fold spelled out on `FlowRunSummarySchema` below. That is also what makes a + * fabricated count the expensive kind of wrong: the fold is the step that would + * have settled it, and a faked `acted` is a fact the fold can only repeat. * * [#4395] "Declares nothing" is now the fallback rather than the only case: * `ConnectorActionSchema.effect` lets an action say `read` or `write`, and a diff --git a/packages/spec/src/automation/flow-function.test.ts b/packages/spec/src/automation/flow-function.test.ts index 75685d7f9c..13ba40e3cc 100644 --- a/packages/spec/src/automation/flow-function.test.ts +++ b/packages/spec/src/automation/flow-function.test.ts @@ -191,7 +191,11 @@ describe('defineStack({ functions }) — the authoring surface (#4396)', () => { // ignores everything else by construction, so a misspelled `effect` was // dropped at the schema and then not looked for — and the failure is the quiet // direction: the function registers, runs, and its writes are counted as none, -// keeping #4354's broken-sweep query silent on the one run that needed it. +// so the run reports `selected > 0, acted 0, unmeasured 0` — which SATISFIES +// #4354's broken-sweep FIRST FILTER. The run lands in the candidate set +// reading exactly like a dead sweep, on a flow that did its work; the `effect` +// declaration is what would have kept it out (`unmeasured > 0`), and the +// misspelling is what dropped it. describe('unknown keys are rejected, not stripped (#4001 batch 11)', () => { const base = { manifest: { id: 'com.example.demo', name: 'demo', version: '1.0.0', type: 'app' as const }, diff --git a/packages/spec/src/automation/flow-function.zod.ts b/packages/spec/src/automation/flow-function.zod.ts index 0d2bcc24a7..2435835195 100644 --- a/packages/spec/src/automation/flow-function.zod.ts +++ b/packages/spec/src/automation/flow-function.zod.ts @@ -141,8 +141,12 @@ export const DEFAULT_FLOW_FUNCTION_EFFECT: FlowFunctionEffect = 'pure'; * construction, so before this change a misspelled `effect` was dropped at the * schema and then *not looked for* by the reader — and the failure is the quiet * direction: the function is registered, it runs, and its writes are counted as - * none, so #4354's broken-sweep filter stays silent on the one flow that needed - * it. (A misspelled `effect` VALUE — `'write'` — already fails loudly-ish: + * none, so the run reports `selected > 0, acted 0, unmeasured 0` and lands + * INSIDE #4354's broken-sweep first filter, reading exactly like the broken + * sweep that filter exists to detect — on the one flow that was doing its work + * through the function. Declaring `effect: 'writes'` is what would have kept it + * out (`unmeasured > 0`); the misspelling is what dropped the declaration. + * (A misspelled `effect` VALUE — `'write'` — already fails loudly-ish: * `normalizeFlowFunctionEntry` degrades it to `'writes'` and surfaces the raw * string. A misspelled KEY had no such backstop.) */ @@ -173,7 +177,8 @@ export const FlowFunctionDeclarationSchema = lazySchema(() => strictObject({ 'Until this shape was closed, these were dropped silently — and `normalizeFlowFunctionEntry` reads only ' + '`handler`/`effect` by construction, so a misspelled `effect` was discarded twice over: ' + 'the function still registered, still ran, and its writes were still counted as none, ' + - 'which is what keeps the broken-sweep filter quiet on the run that needed it.', + 'which is what drops the run INTO the broken-sweep first filter ' + + '(`selected > 0 AND acted = 0 AND unmeasured = 0`) on the very flow whose work the declaration would have accounted for.', }, { handler: z.function().describe('The function invoked by name (a `script` node, a string-named Hook/Action handler)'), effect: FlowFunctionEffectSchema.default(DEFAULT_FLOW_FUNCTION_EFFECT)