From 4e9159f5eb2eeab003cd5895fdf251f2898ff101 Mon Sep 17 00:00:00 2001 From: Mike Clay Date: Tue, 25 Aug 2026 06:28:02 +0100 Subject: [PATCH] Count a loop's collection as a consumer of what produces it A forEach loop names the collection it iterates, and that name is the one place the collection is read. Binding fidelity now records it as a consumption site, so a technique output reaching a loop and nothing else is live rather than dead. The name resolves against its head, because a loop iterates a field of a produced object as often as the object itself. The corpus reports the same seventy findings before and after, all triaged, so the change adds sight rather than findings. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/check-binding-fidelity.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/scripts/check-binding-fidelity.ts b/scripts/check-binding-fidelity.ts index 4bceacd38..3d779934b 100644 --- a/scripts/check-binding-fidelity.ts +++ b/scripts/check-binding-fidelity.ts @@ -17,8 +17,9 @@ * satisfy a read here. * * (3) dead-output — an output id declared in an op's own file that nothing OUTSIDE that file - * consumes: no `{token}` read, no condition variable, no step-binding value or remap, and - * no same-named declared input (the name-match chaining convention). A mention only in the + * consumes: no `{token}` read, no condition variable, no step-binding value or remap, no + * loop `over` collection, and no same-named declared input (the name-match chaining + * convention). A mention only in the * declaring file's own protocol ("return `{x}`") is internal wiring, not a consumer. * Outputs carrying an `#### artifact` block are exempt — the server consumes them when it * synthesizes the activity artifact contract. @@ -338,6 +339,13 @@ function walkSteps(wf: string, rel: string, node: unknown, activityId: string, s const eff = o.effect as { setVariable?: object } | undefined; if (eff?.setVariable) Object.keys(eff.setVariable).forEach((k) => produced(wf).add(k)); if (typeof o.variable === 'string') produced(wf).add(o.variable); + // A `forEach` loop's `over` names the collection it iterates, which is the one place that + // collection is read. Its producer has a consumer here, and a collection reaching the loop + // through no other name is live. `over` reaches a field of a produced object as often as the + // object itself (`implementation_plan.tasks`), so it resolves against its head like any read. + if (typeof o.over === 'string') { + expressionConsumes.push({ rel, stepId: here, name: o.over.split('.')[0]! }); + } for (const v of Object.values(o)) walkSteps(wf, rel, v, activityId, here); } @@ -532,8 +540,9 @@ function scopeOf(wf: string): Set { } /** Where each name is consumed: `{token}` reads and condition variables, binding remap keys, - * binding input values (bare names and embedded tokens), and same-named declared inputs (the - * name-match chaining convention) — keyed by consuming file. Liveness for the dead-output check + * binding input values (bare names and embedded tokens), loop `over` collections, and same-named + * declared inputs (the name-match chaining convention) — keyed by consuming file. Liveness for + * the dead-output check * is consumption OUTSIDE the declaring file: an output mentioned only by its own protocol prose * ("return `{x}`") has no downstream consumer. */ function collectConsumedSites(): Map> {