From 359ccc4ba99bd6f7cc5b46d5aa5fdc9ce236a466 Mon Sep 17 00:00:00 2001 From: robbond Date: Tue, 11 Aug 2026 13:22:29 +0100 Subject: [PATCH] prompt: remove semantic-only mutation conflict --- lib/graph/prompt-builder.js | 5 +- tests/graph/prompt-builder.test.js | 103 +++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) diff --git a/lib/graph/prompt-builder.js b/lib/graph/prompt-builder.js index 8007012..7d7d53e 100644 --- a/lib/graph/prompt-builder.js +++ b/lib/graph/prompt-builder.js @@ -128,8 +128,9 @@ The JSON object must contain exactly these top-level fields: - If you add a new unknown, do not leave it floating: connect it with an added edge to the relevant decision/context node created or updated from the answer. - If you add a new unknown, its description must do two jobs in one sentence: what is unknown, and why resolving it matters for the case. - Treat selectedQuestion as a candidate only; the engine will apply deterministic information-value scoring after validation. -- If the answer does not justify a change, return empty arrays for every category. -- Use answerMeaning to preserve the answer's direct meaning even when the graph change remains unresolved. +- If rule #6 does not apply (the answer contains no user-supported meaning that requires graph progress) and there is no other justification for change, return empty arrays for every category. +- If rule #6 applies but you choose an update/refinement of existing structure, resolve an existing unknown, or add justified new structure, your structural proposal plus answerMeaning together represent the complete response — answerMeaning preserves semantic fidelity while structural mutation handles graph progress; neither replaces the other. +- If you add a new unknown with addedNodes, connect it with at least one addedEdge to an existing updated/resolved node or to a newly added non-unknown node from the answer. ## Example Constraint Reminder ${formatExampleAnswerBlock()} diff --git a/tests/graph/prompt-builder.test.js b/tests/graph/prompt-builder.test.js index 43acba7..fcd9064 100644 --- a/tests/graph/prompt-builder.test.js +++ b/tests/graph/prompt-builder.test.js @@ -177,4 +177,107 @@ describe("buildGraphUpdatePrompt — semantic-to-mutation MUST rule", () => { ); expect(mustRuleMatch[0]).toContain("userSupportedMeaning"); }); + + // ── 57J.43 — no surviving semantic-only/no-op conflict ── + + it("PASS: no direct contradiction — MUST rule is not undermined by empty-array permission", () => { + const prompt = buildGraphUpdatePrompt(makeContext()); + + // The MUST rule must exist... + expect(prompt).toContain( + "MUST express its effect through structural mutation", + ); + + // ...and the empty-array permission must NOT be unconditional. + // It must reference rule #6 as a condition, meaning it cannot apply + // when the MUST rule fires. + const additionalGuidance = prompt.split("## Additional Guidance")[1]; + + // The old conflicting wording must be absent: + expect(additionalGuidance).not.toContain( + "If the answer does not justify a change, return empty arrays", + ); + + // The new permission must reference rule #6: + expect(additionalGuidance).toContain("rule #6"); + }); + + it("PASS: legitimate true no-op preserved — empty mutation allowed when rule #6 does not apply", () => { + const prompt = buildGraphUpdatePrompt(makeContext()); + const additionalGuidance = prompt.split("## Additional Guidance")[1]; + + // The corrected bullet must still allow empty arrays, but only + // when rule #6 does not apply (no consequential meaning). + expect(additionalGuidance).toContain( + "return empty arrays for every category", + ); + // And it must be conditioned: + expect(additionalGuidance).toContain("does not apply"); + }); + + it("PASS: answerMeaning is not structural progress", () => { + const prompt = buildGraphUpdatePrompt(makeContext()); + const additionalGuidance = prompt.split("## Additional Guidance")[1]; + + // Must explicitly separate answerMeaning from graph mutation: + expect(additionalGuidance).toContain("preserves semantic fidelity"); + // And must not say answerMeaning alone can substitute for mutation: + expect(additionalGuidance).not.toContain( + "even when the graph change remains unresolved", + ); + }); + + it("PASS: duplicate protection preserved — no weakening of existing-duplicate rules", () => { + const prompt = buildGraphUpdatePrompt(makeContext()); + // Rule #4 and rule #11 must still exist with their substance: + expect(prompt).toContain("genuinely new concepts"); + expect(prompt).toContain("duplicate unknowns"); + // Additional Guidance preference for update over add: + const additionalGuidance = prompt.split("## Additional Guidance")[1]; + expect(additionalGuidance).toContain("prefer updatedNodes and resolvedUnknownNodeIds over creating duplicate nodes"); + }); + + it("PASS: update/refine route preserved — no new mandatory-add requirement", () => { + const prompt = buildGraphUpdatePrompt(makeContext()); + + // The MUST rule permits update/refine (not just add): + expect(prompt).toContain("update/refinement of existing structure"); + // Additional guidance still encourages preferring updates: + const additionalGuidance = prompt.split("## Additional Guidance")[1]; + expect(additionalGuidance).toContain("prefer updatedNodes and resolvedUnknownNodeIds over creating duplicate nodes"); + expect(additionalGuidance).toContain("update that node rather than creating only a parallel observation"); + }); + + it("PASS: possibleInference separation preserved — not converted to mandatory mutation", () => { + const fullPrompt = buildGraphUpdatePrompt(makeContext()); + + // Rule #27 must still separate possibleInference from userSupportedMeaning: + expect(fullPrompt).toContain( + "Put any stronger interpretation in answerMeaning.possibleInference, not in userSupportedMeaning", + ); + + // The corrected Additional Guidance must reference the mutation trigger via rule #6 + // (which itself references userSupportedMeaning), not possibleInference: + const additionalGuidance = fullPrompt.split("## Additional Guidance")[1]; + expect(additionalGuidance).toContain("rule #6"); + expect(additionalGuidance).not.toContain("possibleInference"); + + // Rule #27 exists in the prompt (separation preserved): + expect(fullPrompt).toContain("answerMeaning.possibleInference, not in userSupportedMeaning"); + }); + + it("PASS: no action-selection machinery added — no keyword routing or node-kind decision table", () => { + const fullPrompt = buildGraphUpdatePrompt(makeContext()); + + // Confirm we did not add new provider-specific routing: + expect(fullPrompt).not.toContain("qwen"); + expect(fullPrompt).not.toContain("claude"); + expect(fullPrompt).not.toContain("gpt"); + + // No keyword-based node-kind decision table: + expect(fullPrompt).not.toContain("keyword"); + + // No mandatory-add logic: + expect(fullPrompt).not.toContain("must add a new node"); + }); });