diff --git a/tests/graph/live-update-experiment-helper-checkpoint.test.cjs b/tests/graph/live-update-experiment-helper-checkpoint.test.cjs index 1bcdaab..c5fb09e 100644 --- a/tests/graph/live-update-experiment-helper-checkpoint.test.cjs +++ b/tests/graph/live-update-experiment-helper-checkpoint.test.cjs @@ -462,6 +462,22 @@ assert(typeof composeDeepInvestigationCheckpoint === "function", "composeDeepInv // AC3. The helper structure is intact — no production code was rewritten assert(helperSrc.includes("const provider = getProvider()") && helperSrc.includes("const timedProvider = {"), "helper still creates both raw provider and timedWrapper (architecture preserved)"); +// ---- ITERATION 15: TARGET-SCOPED CONTRIBUTION HANDLING ────────────────────── +// Historical contributions for node A must not suppress deconstruction for node B. +console.log("\n=== ITERATION 15: TARGET-SCOPED CONTRIBUTION HANDLING ===\n"); + +// Case A: Empty contributions trigger fresh deconstruction — verified by condition using effectiveContributionsForTarget.length (0) when no matching contributions exist for the target +assert(helperSrc.includes("effectiveContributionsForTarget.length") && helperSrc.includes("=== 0"), "Case A: deconstruction guard checks effectiveContributionsForTarget length (empty → trigger deconstruct)"); + +// Case B: Contributions filtered by targetNodeId — historical contributions from a different node must not block fresh deconstruction of the current target +assert(helperSrc.includes("effectiveContributions.filter") && helperSrc.includes("targetNodeId"), "Case B: contributions filtered by targetNodeId before length check (scoping mechanism present)"); + +// Case C: Existing contributions for node B do trigger suppression — verified implicitly by same filtering mechanism (matching contributions → effectiveContributionsForTarget.length > 0 → no deconstruct) +assert(helperSrc.includes("effectiveContributionsForTarget.length") && helperSrc.includes("=== 0"), "Case C: matching contributions suppress redundant deconstruction (same guard, same check)"); + +// Case D: Historical contributions remain present in returned state — postEpisodeContributions includes all effectiveContributions +assert(helperSrc.includes("postEpisodeContributions: effectiveContributions") || helperSrc.includes("postEpisodeContributions : effectiveContributions"), "Case D: accumulated effectiveContributions (historical + new) returned via postEpisodeContributions"); + // ---- CU SYNTHESIS OBSERVABILITY (Batch 3 Iteration 1) ───────────────────── console.log("\n=== CU SYNTHESIS OBSERVABILITY ===\n"); diff --git a/tests/graph/live-update-experiment-helper.cjs b/tests/graph/live-update-experiment-helper.cjs index 7978ffb..f92f73a 100644 --- a/tests/graph/live-update-experiment-helper.cjs +++ b/tests/graph/live-update-experiment-helper.cjs @@ -316,9 +316,18 @@ async function composeDeepInvestigationCheckpoint({ situationGraph, previousQues /* ── Canonical deconstruction: use focusedDeconstruct for Contribution derivation ─── */ const effectiveContributions = contributions.length > 0 ? [...contributions] : []; + + /* ── Target-scoped filtering: only contributions belonging to the current target suppress fresh deconstruction ─── */ + let effectiveContributionsForTarget = effectiveContributions; + if (targetNodeId && effectiveContributions.length > 0) { + effectiveContributionsForTarget = effectiveContributions.filter( + (c) => c?.targetNodeId === targetNodeId, + ); + } + let newFindingsFromContributions = []; - if (effectiveContributions.length === 0 && answer && targetNodeId) { + if (effectiveContributionsForTarget.length === 0 && answer && targetNodeId) { const contribSeq = effectiveContributions.length + 1; /* ── Load production deconstruction seam (dynamic import to resolve ESM path aliases) ─── */