diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index 48dc9a7..8b37722 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -22,3 +22,4 @@ {"id":"int-03ed1c823da90551f51a5a0d5691e440","kind":"field_change","created_at":"2026-08-28T12:35:29.257332022Z","actor":"Shinuwa","issue_id":"sokko-g-39h","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Applied NDS event-surface to calendar week and month events, documented data-color/data-size, and validated checks."}} {"id":"int-88c39f90fdd5b81110acc4bdae55836c","kind":"field_change","created_at":"2026-09-10T11:49:36.768770213Z","actor":"Shinuwa","issue_id":"sokko-g-vfo","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Shared lazy React registry consumed by toolbox and library; obsolete static import assertions removed; all 17 library types exercised; npm run check and 13 E2E tests pass."}} {"id":"int-7ec422ecc24735e12910a965567f32fe","kind":"field_change","created_at":"2026-09-10T12:17:38.573656548Z","actor":"Shinuwa","issue_id":"sokko-g-531","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Concurrent image batches merge current data after compression; calendar selection handlers update drafts outside state updaters. Behavioral image test and reverse week/month selection E2E pass; npm run check passes."}} +{"id":"int-1668982a1dda5d8304440ae699a32d40","kind":"field_change","created_at":"2026-09-10T12:22:11.969660039Z","actor":"Shinuwa","issue_id":"sokko-g-5u5","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Recursive cache enabled for completed evaluations. Before: 131054 reads for 16 cells; after: 50 reads for 50 cells. Regression test failed before fix and passes after; npm run check and table persistence E2E pass."}} diff --git a/tests/table-evaluation.test.mjs b/tests/table-evaluation.test.mjs new file mode 100644 index 0000000..c71f63b --- /dev/null +++ b/tests/table-evaluation.test.mjs @@ -0,0 +1,53 @@ +// Rôle : vérifie le coût des références répétées et les erreurs du tableau. +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { evaluateTable, evaluateTableCell } from "../website/src/features/toolboxes/modules/tableFormulaEngine.js"; + +for (const reverse of [false, true]) { + test(`table reuses repeated references with ${reverse ? "forward" : "backward"} dependencies`, () => { + const count = 50; + const raw = { [reverse ? "A50" : "A1"]: "1" }; + for (let i = 1; i < count; i++) { + const row = reverse ? count - i : i + 1; + const dependency = reverse ? row + 1 : row - 1; + raw[`A${row}`] = `=A${dependency}+A${dependency}`; + } + let reads = 0; + const cells = new Proxy(raw, { + get(target, address) { + // Stop an exponential regression before it can hang the test runner. + if (++reads > count * 4) throw new Error("Read budget exceeded"); + return target[address]; + } + }); + const dimensions = { rows: count, columns: 1 }; + const last = reverse ? "A1" : "A50"; + assert.equal(evaluateTableCell(last, cells, dimensions).value, 2 ** 49); + assert.ok(reads <= count * 4); + reads = 0; + assert.equal(evaluateTable(cells, dimensions)[last].value, 2 ** 49); + assert.ok(reads <= count * 4); + raw[reverse ? "A50" : "A1"] = "2"; + reads = 0; + assert.equal(evaluateTable(cells, dimensions)[last].value, 2 ** 50); + }); +} + +test("shared evaluation preserves cycle, division and invalid reference errors", () => { + const cells = { + A1: "=B1", B1: "=A1", C1: "=A1+1", + A2: "=1/0", B2: "=A2", C2: "=Z99", + A3: "text", B3: "=A3", C3: "=7+5" + }; + const dimensions = { rows: 3, columns: 3 }; + const result = evaluateTable(cells, dimensions); + assert.deepEqual(Object.fromEntries(Object.entries(result).map(([key, value]) => [key, value.error])), { + A1: "cycle", B1: "cycle", C1: "cycle", + A2: "division", B2: "reference", C2: "reference", + A3: "", B3: "reference", C3: "" + }); + for (const address of Object.keys(cells)) { + assert.deepEqual(result[address], evaluateTableCell(address, cells, dimensions)); + } + assert.equal(result.C3.value, 12); +}); diff --git a/website/src/features/toolboxes/modules/tableFormulaEngine.js b/website/src/features/toolboxes/modules/tableFormulaEngine.js index 0705e73..d51b078 100644 --- a/website/src/features/toolboxes/modules/tableFormulaEngine.js +++ b/website/src/features/toolboxes/modules/tableFormulaEngine.js @@ -166,17 +166,22 @@ export function evaluateTableCell(address, cells, dimensions, stack = []) { } function evaluateTableCellWithCache(address, cells, dimensions, stack, cache) { - if (!stack.length && cache.has(address)) return cache.get(address); - const raw = String(cells?.[address] || "").trim(); - if (!raw) return { value: null, display: "", error: "empty" }; - if (stack.includes(address)) return { value: null, display: "#CYCLE", error: "cycle" }; + if (cache.has(address)) return cache.get(address); + const raw = String(cells?.[address] || "").trim(); + if (!raw) { + const evaluated = { value: null, display: "", error: "empty" }; + cache.set(address, evaluated); + return evaluated; + } if (!raw.startsWith("=")) { const value = parsePlainNumber(raw); - return value == null + const evaluated = value == null ? { value: null, display: raw, error: "" } : { value, display: formatNumber(value), error: "" }; + cache.set(address, evaluated); + return evaluated; } const result = evaluateFormula(raw.slice(1), (reference) => { @@ -189,12 +194,12 @@ function evaluateTableCellWithCache(address, cells, dimensions, stack, cache) { if (result.error) { const label = result.error === "cycle" ? "#CYCLE" : result.error === "division" ? "#DIV/0" : "#ERREUR"; const evaluated = { value: null, display: label, error: result.error }; - if (!stack.length) cache.set(address, evaluated); + cache.set(address, evaluated); return evaluated; } const evaluated = { value: result.value, display: formatNumber(result.value), error: "" }; - if (!stack.length) cache.set(address, evaluated); + cache.set(address, evaluated); return evaluated; }