From ca576626e349ebe7777ded97152172ba96485303 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Sun, 1 Nov 2026 15:11:58 +0800 Subject: [PATCH] fix(goals): preserve outcome positions during refinement Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../goals/outcome_requirements.ts | 7 ++++-- .../test_goal_outcome_refinement.py | 11 +++++++++ .../goal_outcome_refinement.test.ts | 24 +++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/loopx/control_plane/goals/outcome_requirements.ts b/loopx/control_plane/goals/outcome_requirements.ts index 1d9ba9857b..1dad34811c 100644 --- a/loopx/control_plane/goals/outcome_requirements.ts +++ b/loopx/control_plane/goals/outcome_requirements.ts @@ -181,8 +181,11 @@ export function refineGoalOutcome(head: JsonObject, goalId: string, goalRef: Wir requireValue(items.length === outcome.requirements.length && items.every(row => reqs.has(row.id)), "review must address every retained requirement exactly once"); outcome.review = {basis_digest: reviewBasis(outcome, todos), items}; } - const state: OutcomeState = {schema_version: GOAL_OUTCOMES_SCHEMA, goal_id: goalId, goal_ref: goalRef, revision: (previous?.revision ?? 0) + 1, - outcomes: [...(previous?.outcomes.filter(o => o.id !== outcomeId) ?? []), outcome]}; + // Refinement changes one outcome, not the positions of unrelated requirements + // in canonical snapshots and their derived full/delta context. + const outcomes = previous?.outcomes.map(row => row.id === outcomeId ? outcome : row) ?? []; + if (!old) outcomes.push(outcome); + const state: OutcomeState = {schema_version: GOAL_OUTCOMES_SCHEMA, goal_id: goalId, goal_ref: goalRef, revision: (previous?.revision ?? 0) + 1, outcomes}; // Preserve declaration/review limits; diagnostics count the failing // envelope, not the combined snapshot or an individual outcome/source list. const candidate = outcomeBytes(state); diff --git a/tests/control_plane/test_goal_outcome_refinement.py b/tests/control_plane/test_goal_outcome_refinement.py index 7ab1efb57b..f883fafdf8 100644 --- a/tests/control_plane/test_goal_outcome_refinement.py +++ b/tests/control_plane/test_goal_outcome_refinement.py @@ -129,6 +129,11 @@ def test_cumulative_review_evidence_reuses_real_quota_sources_without_hiding_new "note": ("Retain the measured result and its applicability. " * 10).strip()} for r in declared["requirements"]]} (code, result), _ = record(project, cli, review, "review") assert code == 0, result + companion = {**declared, "outcome_id": "compatibility", "objective": "Keep the compatible export", + "requirements": [{"id": f"tail{i}", "description": ("Keep the compatibility tail. " * 16).strip()} + for i in range(12)]} + (code, companion_result), _ = record(project, cli, companion, "companion") + assert code == 0, companion_result def capture(): code, packet = run("quota", "should-run", "--goal-id", "goal-acceptance", "--agent-id", "agent-a") @@ -141,11 +146,17 @@ def capture(): review["review"][0]["conclusion"] = "unmet" assert record(project, cli, review, "negative-review")[0][0] == 0 current = capture() + def outcomes(context): + return next(s["content"]["goal_acceptance_contract"]["outcome_requirements"]["outcomes"] + for s in context["sources"] if s.get("source") == "goal_acceptance") full = prepare_context_delivery(current, scope=scope, capture_id="current") delta = prepare_context_delivery(current, scope=scope, capture_id="current", baseline=baseline, retained={"complete": True, "target": baseline["target"]}) assert delta["mode"] == "delta", delta.get("reason") assert "segment/" not in json.dumps(delta), "unchanged evidence must not be retransmitted" + assert "Keep the compatibility tail" not in json.dumps(delta), "an unrelated outcome must not be retransmitted" + assert [row["id"] for row in outcomes(current)] == [row["id"] for row in outcomes(original)] + assert outcomes(current)[1] == outcomes(original)[1], "another outcome's requirements and evidence stay intact" restored = restore_context_delivery(delta, baseline=baseline, current=full) assert restored["context"] == current acceptance = next(s["content"] for s in restored["context"]["sources"] if s.get("source") == "goal_acceptance") diff --git a/tests/control_plane_ts/goal_outcome_refinement.test.ts b/tests/control_plane_ts/goal_outcome_refinement.test.ts index f610d7cd1f..c251a22112 100644 --- a/tests/control_plane_ts/goal_outcome_refinement.test.ts +++ b/tests/control_plane_ts/goal_outcome_refinement.test.ts @@ -64,6 +64,30 @@ test("refinement retains original intent, accumulates requirements and cannot in {...goalRef, goal_instance_id: "ginst_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}, "author", patch()), /lifetime/); }); +for (const outcomeId of ["tour", "middle", "last"]) test(`refining ${outcomeId} preserves unrelated outcome positions and content`, () => { + const head = fixture(); + for (const id of ["tour", "middle", "last"]) + head.goal_outcomes = refineGoalOutcome(head, goal, null, "author", patch({outcome_id: id})); + const before = structuredClone(head); + const next = refineGoalOutcome(head, goal, null, "author", {outcome_id: outcomeId, + review: [{id: "opening", conclusion: "unmet", note: "The opening still needs correction.", evidence_refs: ["artifact:check"]}]}); + assert.deepEqual(head, before, "refinement cannot mutate its source snapshot"); + assert.deepEqual(next.outcomes.map(row => row.id), ["tour", "middle", "last"]); + for (const row of next.outcomes) { + const prior = before.goal_outcomes.outcomes.find((item: JsonObject) => item.id === row.id); + if (row.id !== outcomeId) assert.deepEqual(row, prior); + else { + assert.equal(row.revision, prior.revision + 1); + assert.deepEqual(row.requirements, prior.requirements); + assert.deepEqual(row.sources, prior.sources); + assert.equal(row.review!.items[0].conclusion, "unmet"); + } + } + const appended = refineGoalOutcome({...head, goal_outcomes: next}, goal, null, "author", patch({outcome_id: "new"})); + assert.deepEqual(appended.outcomes.map(row => row.id), ["tour", "middle", "last", "new"]); + assert.deepEqual(appended.outcomes.slice(0, 3), next.outcomes, "a new outcome cannot rewrite existing ones"); +}); + test("only matching current acceptance and work coverage can qualify requirements", () => { const head = fixture(); head.goal_outcomes = refineGoalOutcome(head, goal, null, "author", patch()); const accepted = {enabled: true, status: "accepted", criteria: [{id: "opening", description: "Keep the opening"}],