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
35 changes: 35 additions & 0 deletions .changeset/broken-sweep-residue-first-filter.md
Original file line numberDiff line numberDiff line change
@@ -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.
9 changes: 7 additions & 2 deletions packages/spec/src/automation/execution.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 }],
Expand Down
17 changes: 13 additions & 4 deletions packages/spec/src/automation/execution.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,10 +55,19 @@ export type ExecutionStatus = z.input<typeof ExecutionStatus>;
* 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
Expand Down
6 changes: 5 additions & 1 deletion packages/spec/src/automation/flow-function.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 },
Expand Down
11 changes: 8 additions & 3 deletions packages/spec/src/automation/flow-function.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.)
*/
Expand DownExpand Up@@ -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)
Expand Down
Loading