From 5c106cf033c0e5a462fc886ed57f2dbb0e01e5ef Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 6 Nov 2026 00:43:15 +0200 Subject: [PATCH] fix(giellie): a dry run is a plan, not an outcome The engine reports a row as applied on a dry run when it only would be applied, so the preview now says so and states that nothing has changed yet. The prompt also maps the answer to the mode value and names the fields the catalogue and the till each keep. --- src/lib/server/giellie-system-prompt.test.ts | 26 ++++++++++++++++++-- src/lib/server/giellie-system-prompt.ts | 6 ++--- src/lib/server/giellie-tool-policy.test.ts | 21 ++++++++++++++++ 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/lib/server/giellie-system-prompt.test.ts b/src/lib/server/giellie-system-prompt.test.ts index befc99a..c51671c 100644 --- a/src/lib/server/giellie-system-prompt.test.ts +++ b/src/lib/server/giellie-system-prompt.test.ts @@ -117,6 +117,20 @@ describe("buildSystemPrompt", () => { expect(p).toMatch(/SKU, item, before, after, status/i); }); + // On a dry run the engine reports "applied" for every row that WOULD + // change. If the model renders that verbatim, the preview reads as an + // outcome and the quantities are silently never written. + it("makes the dry-run preview a plan, not an outcome", () => { + expect(p).toMatch(/a dry run writes nothing/i); + expect(p).toMatch(/"applied" only means "would be applied"/i); + expect(p).toMatch(/nothing has changed yet/i); + }); + + it("maps the answer to the mode value", () => { + expect(p).toMatch(/mode "set"/); + expect(p).toMatch(/mode "add"/); + }); + it("reports the lines that did not resolve", () => { expect(p).toMatch(/unmatched/i); expect(p).toMatch(/ambiguous/i); @@ -129,9 +143,17 @@ describe("buildSystemPrompt", () => { // The catalogue opt-in rule already lives in the pricing playbook. The // quantities section points at it rather than repeating it. - it("does not restate the price_list opt-in rule", () => { + it("states the price_list opt-in rule once and does not repeat it", () => { + // The rule is present, and the single surviving statement of it is the + // pricing playbook's — asserted on its own so a rewording of the + // pricing rule cannot be mistaken for a missing rule. + expect(p).toMatch(/NEVER write to the price_list catalogue/i); + expect(p).toMatch(/unless the user has explicitly asked/i); + // … and it is stated exactly once. expect((p.match(/unless the user has explicitly asked/gi) ?? []).length).toBe(1); - expect(p).toMatch(/never sets a quantity on the counter/i); + // The quantities section names the fields instead of restating the rule. + expect(p).toMatch(/price_list\.add_items writes only the supplier catalogue/i); + expect(p).toMatch(/the till quantity lives on stock_items/i); }); }); }); diff --git a/src/lib/server/giellie-system-prompt.ts b/src/lib/server/giellie-system-prompt.ts index bd0ab25..a1d1602 100644 --- a/src/lib/server/giellie-system-prompt.ts +++ b/src/lib/server/giellie-system-prompt.ts @@ -29,14 +29,14 @@ Stock items and pricing (POS): Stock quantity updates (POS): When the user pastes stock lines (a SKU or description with a quantity): -1. Always ask first whether the numbers are the new quantity on hand or a change to add to the current stock. Do not infer it from the wording, and do not write anything until they answer. +1. Always ask first whether the numbers are the new quantity on hand or a change to add to the current stock. Do not infer it from the wording, and do not write anything until they answer. Send mode "set" for the new quantity on hand and mode "add" for a change to add to the current stock. 2. Resolve the lines with stock_items.set_quantities. Never loop stock_items.list and stock_items.adjust by hand — that path takes a signed delta, has no batch call, and writes a ledger row with no actor. -3. Call it with dry_run: true first and show the user the plan as a table: SKU, item, before, after, status. Point out the lines that did not match and the ones that were ambiguous. +3. Call it with dry_run: true first and show the user the plan as a table: SKU, item, before, after, status. A dry run writes nothing, so a row whose status is "applied" only means "would be applied" — render it as "would be applied", label the table a plan, and say plainly that nothing has changed yet. Point out the lines that did not match and the ones that were ambiguous. 4. Only when they say to apply, call it again with dry_run: false. 5. Report the outcome: applied, unchanged, unmatched, ambiguous, failed. 6. Quantities may be negative and nothing blocks on a low quantity. The till oversells on purpose and the stocktake is what corrects it. -Quantities are not catalogue work: price_list.add_items never sets a quantity on the counter. +Quantities are not catalogue work: price_list.add_items writes only the supplier catalogue (price_list.stock_qty), and the till quantity lives on stock_items (stock_items.current_qty). Style: plain, concise South African business English. Lead with the answer. Use tables for lists of records. Include order / quote / PO numbers so the user can find things. Amounts as "R12,345.67".`; } diff --git a/src/lib/server/giellie-tool-policy.test.ts b/src/lib/server/giellie-tool-policy.test.ts index e075966..23b48fc 100644 --- a/src/lib/server/giellie-tool-policy.test.ts +++ b/src/lib/server/giellie-tool-policy.test.ts @@ -257,4 +257,25 @@ describe("stock_items.set_quantities", () => { }).cls, ).toBe("read"); }); + + // The route treats only true / "true" as a dry run, so anything else is a + // real write. A rule widened to "anything that is not false" would read a + // dry_run of "false" as a preview and wave the write past the approval + // card — this pins both false forms to a write. + it("is a write when dry_run is false, the boolean or the string", () => { + expect( + classifyToolCall("stock_items.set_quantities", { + mode: "set", + lines: [], + dry_run: false, + }).cls, + ).toBe("write"); + expect( + classifyToolCall("stock_items.set_quantities", { + mode: "set", + lines: [], + dry_run: "false", + }).cls, + ).toBe("write"); + }); });