Skip to content

Commit eeba2ef

Browse files
os-trumpclaude
andauthored
docs(spec): the broken-sweep predicate in FlowFunctionEffectSchema is a first FILTER (#13068)
`packages/spec/src/automation/flow-function.zod.ts` stated `selected > 0 AND acted = 0 AND unmeasured = 0` as *the* broken-sweep query, unqualified -- the third `packages/spec` surface carrying the claim, outside the fence of the change that corrected `automation/execution.zod.ts` and `integration/connector.zod.ts`. #12685 measured the A/B on one graph pair through the real engine: a healthy idempotent sweep -- re-select the same records, gate each one on "already handled" -- and a dead gate BOTH report `selected > 0, acted 0, unmeasured 0`. "Over N consecutive runs" does not rescue it: the healthy steady state is persistent for as long as the outstanding work stands, so it trips on every run; consecutiveness filters flapping, which is a different failure. Reworded to the shape the sibling surfaces now agree on: the predicate is the FIRST FILTER, not a verdict, and the per-node fold (`FlowRunSummary.nodes[]` / `gates[]`) is the discriminator. Each clause keeps its own true point -- a declared-`writes` function makes `acted` INCOMPLETE rather than zero, and an under-reported `selected` can still only make the filter quieter, never wrong. This file's `@module` block is emitted verbatim as the intro of `content/docs/references/automation/flow-function.mdx`, so the artifact is regenerated here with `gen:schema && gen:docs` -- never hand-edited. TSDoc and generated-docs prose only -- no behaviour, no schema, no accept-set change. Claude-Session: https://claude.ai/code/session_01LpRNHxWZgSUgVnFT9mQQo4 Co-authored-by: Claude <noreply@anthropic.com>
1 parent e6fd1ca commit eeba2ef

3 files changed

Lines changed: 88 additions & 13 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
docs(spec): `FlowFunctionEffectSchema`'s broken-sweep claim is a first FILTER, not the detector (#12685)
6+
7+
`patch`, and not empty: these doc blocks ship in `.d.ts`, and this file's
8+
`@module` block is emitted verbatim as the intro of
9+
`content/docs/references/automation/flow-function.mdx` — so the sentence an
10+
operator reads while wiring an alert is a published artifact of this package,
11+
regenerated in this change. No schema, no accept set, no runtime behaviour
12+
moves.
13+
14+
## The wrong claim
15+
16+
`packages/spec/src/automation/flow-function.zod.ts` stated
17+
`selected > 0 AND acted = 0 AND unmeasured = 0` as *the* broken-sweep query,
18+
unqualified — the third `packages/spec` surface carrying it, outside the fence
19+
of the change that corrected `automation/execution.zod.ts` and
20+
`integration/connector.zod.ts`. #12685 measured the A/B on one graph pair
21+
through the real engine (pinned in `run-summary.test.ts`): a healthy idempotent
22+
sweep — re-select the same records, gate each one on "already handled" — and a
23+
dead gate BOTH report `selected > 0, acted 0, unmeasured 0`. "Over N
24+
consecutive runs" does not rescue it: the healthy steady state is persistent
25+
for as long as the outstanding work stands, so it trips on every run;
26+
consecutiveness filters flapping, which is a different failure.
27+
28+
The failure mode is silent. An operator who wires an alert to the documented
29+
predicate watches it fire during normal operation and mutes it, leaving a dead
30+
sweep unmonitored *while looking monitored*.
31+
32+
## What the prose says now
33+
34+
The predicate is stated as the FIRST FILTER and the per-node fold
35+
(`FlowRunSummary.nodes[]` / `gates[]`) is named as the discriminator — the same
36+
shape `sys_automation_run`'s field descriptions, `content/docs/automation/
37+
flows.mdx`, `automation/execution.zod.ts` and `integration/connector.zod.ts`
38+
now agree on. Each clause keeps its own true point: a declared-`writes`
39+
function makes `acted` INCOMPLETE rather than zero, which is why
40+
`unmeasuredEffect` keeps the first filter off the flows that call one, and an
41+
under-reported `selected` can still only make the filter quieter, never wrong.
42+
43+
Five mentions in the file were triaged individually; the one that reads
44+
"an under-report reads exactly like the broken sweep #4354 exists to detect"
45+
was left standing, because it claims nothing about the three-clause predicate
46+
and stays true.

content/docs/references/automation/flow-function.mdx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,18 @@ relying on. It is now declared in two halves:
4040
## What is deliberately not here
4141

4242
A blanket `unmeasuredEffect` on every `script` step (the escape hatch #4354
43-
gave `connector_action`) was rejected: it would suppress the broken-sweep
44-
signal on every flow that calls any function, in order to accommodate the
45-
flows that break the rule — paying for a rule-breaker with everyone else's
46-
signal, and fossilizing the violation as supported behaviour.
43+
gave `connector_action`) was rejected: it would drop every flow that calls
44+
any function out of the broken-sweep FIRST FILTER
45+
(`selected > 0 AND acted = 0 AND unmeasured = 0`), in order to accommodate
46+
the flows that break the rule — paying for a rule-breaker with everyone
47+
else's measurement, and fossilizing the violation as supported behaviour.
48+
That predicate is a first filter and not a verdict (#12685: a healthy
49+
idempotent sweep that re-selects the same records and gates each one on
50+
"already handled" matches it on every run too, and what discriminates is the
51+
per-node fold in `FlowRunSummary.nodes[]` / `gates[]`). Dropping out of it is
52+
still costly even so: a run the filter never selects is never folded over
53+
either, so the escape hatch is declared per function rather than granted to
54+
every `script` step.
4755

4856
Nor is this *enforcement*. The runtime hands a function no data reach —
4957
`FlowFunctionContext` in `@objectstack/service-automation` carries

packages/spec/src/automation/flow-function.zod.ts

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,18 @@
3636
* ## What is deliberately not here
3737
*
3838
* A blanket `unmeasuredEffect` on every `script` step (the escape hatch #4354
39-
* gave `connector_action`) was rejected: it would suppress the broken-sweep
40-
* signal on every flow that calls any function, in order to accommodate the
41-
* flows that break the rule — paying for a rule-breaker with everyone else's
42-
* signal, and fossilizing the violation as supported behaviour.
39+
* gave `connector_action`) was rejected: it would drop every flow that calls
40+
* any function out of the broken-sweep FIRST FILTER
41+
* (`selected > 0 AND acted = 0 AND unmeasured = 0`), in order to accommodate
42+
* the flows that break the rule — paying for a rule-breaker with everyone
43+
* else's measurement, and fossilizing the violation as supported behaviour.
44+
* That predicate is a first filter and not a verdict (#12685: a healthy
45+
* idempotent sweep that re-selects the same records and gates each one on
46+
* "already handled" matches it on every run too, and what discriminates is the
47+
* per-node fold in `FlowRunSummary.nodes[]` / `gates[]`). Dropping out of it is
48+
* still costly even so: a run the filter never selects is never folded over
49+
* either, so the escape hatch is declared per function rather than granted to
50+
* every `script` step.
4351
*
4452
* Nor is this *enforcement*. The runtime hands a function no data reach —
4553
* {@link FlowFunctionContext} in `@objectstack/service-automation` carries
@@ -62,12 +70,25 @@ import { strictObject } from '../shared/strict-object';
6270
* - `'writes'` — it performs writes, or dispatches effects, that the platform
6371
* cannot see or count. The step reports
6472
* `ExecutionStepMetrics.unmeasuredEffect`, so the run's `unmeasured` tally
65-
* keeps the broken-sweep query
73+
* keeps the broken-sweep FIRST FILTER
6674
* (`selected > 0 AND acted = 0 AND unmeasured = 0`) off it — and only off
67-
* the flows that actually call such a function.
75+
* the flows that actually call such a function. The third clause earns its
76+
* place for the reason it always did: such a run has an INCOMPLETE `acted`
77+
* count, not a zero one.
78+
*
79+
* A filter, not a verdict (#12685). Even with all three clauses the predicate
80+
* cannot separate a broken sweep from a healthy idempotent one: a sweep that
81+
* re-selects the same records and gates each one on "already handled" matches
82+
* it on every run while that work stands, so "over N consecutive runs" does
83+
* not separate the two either — consecutiveness filters flapping, which is a
84+
* different failure. What separates them is the per-node fold on
85+
* `FlowRunSummary` (`nodes[]` / `gates[]`): a healthy skip is accounted for by
86+
* a read the run performed, while a dead gate skips just as often with nothing
87+
* behind it. Declaring `'writes'` decides which runs enter that first filter;
88+
* it does not make the filter a detector.
6889
*
6990
* There is deliberately no `'reads'` member: an under-reported `selected` can
70-
* only make the broken-sweep alert quieter, never wrong, so declaring it would
91+
* only make the broken-sweep filter quieter, never wrong, so declaring it would
7192
* buy nothing the runtime acts on.
7293
*/
7394
export const FlowFunctionEffectSchema = lazySchema(() => z.enum([
@@ -120,7 +141,7 @@ export const DEFAULT_FLOW_FUNCTION_EFFECT: FlowFunctionEffect = 'pure';
120141
* construction, so before this change a misspelled `effect` was dropped at the
121142
* schema and then *not looked for* by the reader — and the failure is the quiet
122143
* direction: the function is registered, it runs, and its writes are counted as
123-
* none, so #4354's broken-sweep query stays silent on the one flow that needed
144+
* none, so #4354's broken-sweep filter stays silent on the one flow that needed
124145
* it. (A misspelled `effect` VALUE — `'write'` — already fails loudly-ish:
125146
* `normalizeFlowFunctionEntry` degrades it to `'writes'` and surfaces the raw
126147
* string. A misspelled KEY had no such backstop.)
@@ -152,7 +173,7 @@ export const FlowFunctionDeclarationSchema = lazySchema(() => strictObject({
152173
'Until this shape was closed, these were dropped silently — and `normalizeFlowFunctionEntry` reads only ' +
153174
'`handler`/`effect` by construction, so a misspelled `effect` was discarded twice over: ' +
154175
'the function still registered, still ran, and its writes were still counted as none, ' +
155-
'which is what keeps the broken-sweep alert quiet on the run that needed it.',
176+
'which is what keeps the broken-sweep filter quiet on the run that needed it.',
156177
}, {
157178
handler: z.function().describe('The function invoked by name (a `script` node, a string-named Hook/Action handler)'),
158179
effect: FlowFunctionEffectSchema.default(DEFAULT_FLOW_FUNCTION_EFFECT)

0 commit comments

Comments
 (0)