From 058ad2326fb19b0f42f8094b366a515d7be0a381 Mon Sep 17 00:00:00 2001 From: robbond Date: Thu, 20 Aug 2026 05:45:29 +0100 Subject: [PATCH] test(experiment): checkpoint nonlinear branch continuity --- .../rto-nonlinear-branch-continuity.mjs | 395 ++++++++++++++++++ 1 file changed, 395 insertions(+) create mode 100644 scripts/experimental/rto-nonlinear-branch-continuity.mjs diff --git a/scripts/experimental/rto-nonlinear-branch-continuity.mjs b/scripts/experimental/rto-nonlinear-branch-continuity.mjs new file mode 100644 index 0000000..5588e15 --- /dev/null +++ b/scripts/experimental/rto-nonlinear-branch-continuity.mjs @@ -0,0 +1,395 @@ +/** + * RTO.24A — Non-linear Branch Continuity Harness + * + * Purpose: Deterministic proof that a late semantic result for Branch A + * attaches to Branch A without: + * - moving the user's current focus + * - selecting a next question + * - contaminating Branch B + * - rebuilding any whole-investigation state + * + * Design boundary: + * - Standalone experimental runner. + * - Zero production code changes. + * - Zero live model calls. + * - Pure in-memory deterministic simulation. + * - No graph schema changes. No workers, queues, events, or UI. + * + * Flow tested: + * 1. User works in Branch A (contribution captured). + * 2. Semantic task is created for Contribution A (pending). + * 3. User switches to Branch B and creates Contribution B. + * 4. Late result for Contribution A arrives. + * 5. Result attaches to Branch A / Contribution A only. + * 6. User stays in Branch B. No forced navigation. No global rebuild. + */ + +// ─── Deterministic ID generator (no LLM dependency) ──────────────────────── + +function genId(prefix, label) { + let h = 0; + for (let i = 0; i < label.length; i++) { + h = (Math.imul(31, h) + label.charCodeAt(i)) | 0; + } + return `${prefix}-${(Math.abs(h) % 1e6).toString(16).padStart(6, "0")}`; +} + +// ─── Branch / Contribution state model (minimal, no production schema) ────── + +function createBranch(id, label, centralStatement) { + return { + id, + label, + centralStatement, + contributions: [], + incomingResults: [], // semantic results that arrived late + currentSummary: "", + }; +} + +function createContribution(branchId, text, question) { + const contrib = { + id: genId("contrib", text.slice(0, 30)), + branchId, + text, + question: question || null, + status: "captured", // captured -> pending_result -> result_attached + createdAt: Date.now(), + originalText: text, // preserved for mutation check + }; + return contrib; +} + +function createPendingSemanticTask(contribId, branchId) { + return { + id: genId("task", `${branchId}-${contribId}`), + targetContributionId: contribId, + targetBranchId: branchId, + status: "pending", // pending -> completed -> delivered + createdAt: Date.now(), + }; +} + +function createLateResult(contribId, branchId, resultText) { + return { + id: genId("result", resultText.slice(0, 30)), + targetContributionId: contribId, + targetBranchId: branchId, + text: resultText, + attachedTo: null, // will be set by routing logic + arrivedAt: Date.now(), + }; +} + +// ─── Routing logic under test (the core invariant) ────────────────────────── + +/** + * Route a late semantic result to its target branch and contribution. + * + * This is the minimal deterministic function that embodies the "branch continuity" + * invariant: results travel by explicit provenance links, not by global state. + */ +function routeLateResult(lateResult, branches) { + const targetBranch = branches.find(b => b.id === lateResult.targetBranchId); + if (!targetBranch) { + throw new Error(`Route fail: branch ${lateResult.targetBranchId} not found`); + } + + const targetContrib = targetBranch.contributions.find( + c => c.id === lateResult.targetContributionId + ); + if (!targetContrib) { + throw new Error( + `Route fail: contribution ${lateResult.targetContributionId} not found in branch ${targetBranch.id}` + ); + } + + // Attach result to the target branch's incoming results and its contribution + lateResult.attachedTo = { + branchId: targetBranch.id, + contributionId: targetContrib.id, + }; + + if (!targetBranch.incomingResults.find(r => r.id === lateResult.id)) { + targetBranch.incomingResults.push(lateResult); + } + if (!targetContrib.receivedResults) { + targetContrib.receivedResults = []; + } + targetContrib.receivedResults.push(lateResult); + + // Update status flags on the contribution (non-mutating to original text) + targetContrib.status = "result_attached"; + targetContrib.pendingTaskStatus = "delivered"; + + return { attachedTo: lateResult.attachedTo, branchId: targetBranch.id }; +} + +/** + * Simulate user switching active branch. + * This is the operation that MUST NOT be triggered by a late result arrival. + */ +function setActiveFocus(allState, branchId) { + const exists = allState.branches.some(b => b.id === branchId); + if (!exists) { + throw new Error(`Switch fail: branch ${branchId} not found`); + } + allState.activeBranchId = branchId; + return branchId; +} + +// ─── Harness: Fixed Scenario Setup ────────────────────────────────────────── + +function buildFixedScenario() { + const allState = { + activeBranchId: null, // will be set by the flow + branches: [], + pendingTasks: [], + lateResultsQueue: [], // results that have completed processing and are "arriving" + }; + + // ── Branch A: Competitor development ──────────────────────────────── + + const branchA = createBranch( + "branch-a", + "Competitor development", + "We need to assess whether a competitor is actively developing a solution to our core customer problem." + ); + + const contributionA = createContribution( + branchA.id, + "The competitor has hired machine-learning engineers and presented at an industry conference about the same customer problem.", + "What does the competitor's hiring and public presentation indicate about their product development timeline?" + ); + + branchA.contributions.push(contributionA); + allState.branches.push(branchA); + + // ── Pending semantic task for Contribution A ──────────────────────── + + const pendingTaskA = createPendingSemanticTask( + contributionA.id, + branchA.id + ); + contributionA.pendingTaskStatus = "pending"; + allState.pendingTasks.push(pendingTaskA); + + // ── Create Branch B (must exist before switching active focus) ─────── + + const branchB = createBranch( + "branch-b", + "Customer demand", + "We need to understand whether there is sufficient enterprise customer demand to justify a product launch." + ); + allState.branches.push(branchB); // must exist before setActiveFocus validates + + // ── Switch active focus to Branch B ───────────────────────────────── + + setActiveFocus(allState, branchB.id); + + const contributionB = createContribution( + branchB.id, + "One enterprise customer has expressed strong interest but has not yet signed a contract.", + "What would the enterprise customer's decision timeline look like if we requested a pilot program?" + ); + + branchB.contributions.push(contributionB); + allState.branches.push(branchB); + branchB.currentSummary = `Contribution captured for "${branchB.label}". Awaiting semantic analysis.`; + + return allState; +} + +// ─── Simulation: Late Result Arrival ──────────────────────────────────────── + +function simulateLateResultArrival(allState) { + // Get the pending task for Contribution A + const targetTask = allState.pendingTasks.find( + t => t.status === "pending" + ); + if (!targetTask) { + throw new Error("No pending semantic task to complete"); + } + + // Mark task as completed (semantic analysis finished) + targetTask.status = "completed"; + + // Create the late result with the simulated semantic inference output + const lateResult = createLateResult( + targetTask.targetContributionId, + targetTask.targetBranchId, + "The competitor activity makes active competing-product development more plausible, but does not confirm a product or launch." + ); + + // Deliver the result (this simulates what the routing layer does) + routeLateResult(lateResult, allState.branches); + + // Mark task as delivered + targetTask.status = "delivered"; + targetTask.deliveredResultId = lateResult.id; + + // Enqueue for branch notification + allState.lateResultsQueue.push(lateResult); + + return lateResult; +} + +// ─── Assertions ────────────────────────────────────────────────────────────── + +function runAssertions(allState, lateResult) { + const errors = []; + let passCount = 0; + + function assert(condition, message) { + if (!condition) { + errors.push(message); + console.log(` FAIL: ${message}`); + } else { + passCount++; + console.log(` PASS: ${message}`); + } + } + + // ── Retrieve state references ─────────────────────────────────────── + + const branchA = allState.branches.find(b => b.id === "branch-a"); + const branchB = allState.branches.find(b => b.id === "branch-b"); + const contribA = branchA?.contributions?.[0]; + const contribB = branchB?.contributions?.[0]; + + assert(!!branchA, "Branch A exists in state"); + assert(!!branchB, "Branch B exists in state"); + assert(!!contribA, "Contribution A exists in Branch A"); + assert(!!contribB, "Contribution B exists in Branch B"); + + // ── Contribution ownership ────────────────────────────────────────── + + assert(contribA.branchId === "branch-a", `Contrib A branchId = ${contribA?.branchId}`); + assert(contribB.branchId === "branch-b", `Contrib B branchId = ${contribB?.branchId}`); + + // ── Late result attribution ───────────────────────────────────────── + + assert(lateResult.targetBranchId === "branch-a", `Late result targetBranchId = ${lateResult?.targetBranchId}`); + assert(lateResult.targetContributionId === contribA?.id, `Late result targetContributionId = ${lateResult?.targetContributionId} (expected contrib A)`); + + // ── Attachment correctness ────────────────────────────────────────── + + assert(lateResult.attachedTo !== null && lateResult.attachedTo.branchId === "branch-a", + `Late result attached to branch A (${lateResult?.attachedTo?.branchId})`); + assert(lateResult.attachedTo !== null && lateResult.attachedTo.contributionId === contribA?.id, + `Late result attached to contribution A (${lateResult?.attachedTo?.contributedResultId || lateResult?.attachedTo?.contributionId})`); + + // Verify it is NOT attached to Branch B + const branchBResults = branchB?.incomingResults || []; + assert(branchBResults.length === 0, "Late result does NOT attach to Branch B"); + + // ── User focus unchanged ──────────────────────────────────────────── + + assert(allState.activeBranchId === "branch-b", + `Active branch after late result = ${allState.activeBranchId} (expected branch-b)`); + + // ── No forced navigation / no next-question selection ──────────────── + + // Neither contribution should have been auto-modified + assert(contribA.status === "result_attached", "Contrib A status updated to result_attached (not replaced)"); + assert(contribB.status === "captured", `Contrib B status = ${contribB?.status} (should be captured, not modified)`); + + // Original text preserved for both + assert(contribA.text === contribA.originalText, + `Contribution A text unchanged: "${contribA.text}"`); + assert(contribB.text === contribB.originalText, + `Contribution B text unchanged: "${contribB.text}"`); + + // ── Branch A new-result available indicator ────────────────────────── + + const branchANewResultAvailable = (branchA.incomingResults?.length || 0) > 0; + assert(branchANewResultAvailable, "Branch A can show 'new result available'"); + + // ── Late result queue integrity ───────────────────────────────────── + + assert(allState.lateResultsQueue.length === 1, `Late results queue length = ${allState.lateResultsQueue.length}`); + assert(allState.lateResultsQueue[0].id === lateResult.id, "Queue item matches arrived result"); + + // ── No whole-state reconstruction flag ────────────────────────────── + + // If any branch was rebuilt, it would have a rebuild flag set to true. + // Our routing only touches pending task and incoming results — no branches array recreation. + const branchARebuilt = branchA !== allState.branches.find(b => b.id === "branch-a"); + assert(!branchARebuilt, "Branch A was NOT rebuilt (object identity preserved)"); + + // ── Pending tasks cleanup ─────────────────────────────────────────── + + const remainingPending = allState.pendingTasks.filter(t => t.status === "pending"); + assert(remainingPending.length === 0, "No pending semantic tasks remain after delivery"); + + return { errors, passCount }; +} + +// ─── Main harness ──────────────────────────────────────────────────────────── + +async function main() { + const modelCallCount = 0; // deterministic — no provider import, no fetch + + console.log("=== RTO.24A — Non-linear Branch Continuity Harness ===\n"); + console.log("Classification: flow test (deterministic simulation)"); + console.log("Live model calls allowed: 0\n"); + + // ── Phase 1: Build fixed scenario ──────────────────────────────────── + + console.log("--- Phase 1: Fixed Scenario Setup ---"); + const state = buildFixedScenario(); + + const branchA = state.branches.find(b => b.id === "branch-a"); + const branchB = state.branches.find(b => b.id === "branch-b"); + const contribA = branchA.contributions[0]; + const contribB = branchB.contributions[0]; + + console.log(` Branch A: ${branchA.label} (${branchA.id})`); + console.log(` Contribution A: "${contribA.text.slice(0, 60)}..." (${contribA.id})`); + console.log(` Pending semantic task: ${state.pendingTasks[0]?.id} (status: ${state.pendingTasks[0]?.status})`); + + console.log(` Branch B: ${branchB.label} (${branchB.id})`); + console.log(` Contribution B: "${contribB.text.slice(0, 60)}..." (${contribB.id})`); + console.log(` Active focus: ${state.activeBranchId}`); + + // ── Phase 2: Simulate late result arrival ──────────────────────────── + + console.log("\n--- Phase 2: Late Result Arrives ---"); + const lateResult = simulateLateResultArrival(state); + + console.log(` Late result ID: ${lateResult.id}`); + console.log(` Result text: "${lateResult.text}"`); + console.log(` Attached to: branch=${lateResult.attachedTo?.branchId}, contrib=${lateResult.attachedTo?.contributionId}`); + console.log(` Active focus after arrival: ${state.activeBranchId}`); + + // ── Phase 3: Run assertions ────────────────────────────────────────── + + console.log("\n--- Phase 3: Assertions ---"); + const result = runAssertions(state, lateResult); + + // ── Final report ───────────────────────────────────────────────────── + + console.log("\n=== RESULT ==="); + console.log(`Passes: ${result.passCount}`); + if (result.errors.length > 0) { + console.log(`Failures: ${result.errors.length}`); + for (const err of result.errors) { + console.log(` ✗ ${err}`); + } + console.log("\nHarness: FAIL"); + process.exit(1); + } + + console.log(`\nHarness: PASS (${result.passCount} assertions)`); + console.log(`Live model calls: ${modelCallCount}`); + console.log(`Production code changed: NO`); + console.log(`Graph schema changed: NO`); + console.log(`UI changed: NO`); + + return { state, lateResult, assertions: result }; +} + +main().catch((error) => { + console.error("Harness error:", error.message); + process.exit(1); +});