diff --git a/scripts/experimental/rto-branch-scoped-reasoning-retrieval.mjs b/scripts/experimental/rto-branch-scoped-reasoning-retrieval.mjs new file mode 100644 index 0000000..bfa5f74 --- /dev/null +++ b/scripts/experimental/rto-branch-scoped-reasoning-retrieval.mjs @@ -0,0 +1,402 @@ +/** + * RTO.26A — Branch-scoped Reasoning Retrieval Harness + * + * Purpose: Deterministic proof that reasoning records can be retrieved by + * explicit branch provenance, so the active workspace shows only the questions/ + * contributions that belong to the selected branch while other branches remain + * preserved and untouched. + * + * Core operation under test: getBranchReasoning(branchId) + */ + +// ─── Deterministic ID generator ────────────────────────────────────────────── + +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")}`; +} + +// ─── Minimal record model with explicit provenance ──────────────────────────── + +function createQuestion(branchId, text) { + return { + id: genId("question", text.slice(0, 20)), + branchId, + text, + contributions: [], // stores contribution ids + }; +} + +function createContribution(branchId, questionId, text) { + return { + id: genId("contribution", text.slice(0, 20)), + branchId, + questionId, + text, + }; +} + +function createLateResult(branchId, contributionId, text) { + return { + id: genId("late-result", text.slice(0, 20)), + branchId, + contributionId, + text, + }; +} + +// ─── Core operation under test ──────────────────────────────────────────────── + +/** + * getBranchReasoning(branchId) + * Returns only reasoning records whose provenance explicitly identifies that branch. + */ +function getBranchReasoning(records, branchId) { + return records.filter(r => r.branchId === branchId); +} + +// ─── Fixed fixture data ─────────────────────────────────────────────────────── + +function buildFixture() { + const records = []; + + // Branch A: Competitor development + const qA1 = createQuestion( + "branch-competitor-development", + "What evidence suggests competitors are actively developing similar products?" + ); + records.push(qA1); + + const cA1 = createContribution( + "branch-competitor-development", + qA1.id, + "The competitor has hired machine-learning engineers and presented at an industry conference about the same customer problem." + ); + records.push(cA1); + qA1.contributions.push(cA1.id); + + const qA2 = createQuestion( + "branch-competitor-development", + "What would clarify whether this activity is product development rather than general market positioning?" + ); + records.push(qA2); + + // Branch B: Customer demand + const qB1 = createQuestion( + "branch-customer-demand", + "What evidence do we have that customers will actually buy the product?" + ); + records.push(qB1); + + const cB1 = createContribution( + "branch-customer-demand", + qB1.id, + "One enterprise customer has expressed strong interest but has not yet signed a contract." + ); + records.push(cB1); + qB1.contributions.push(cB1.id); + + const qB2 = createQuestion( + "branch-customer-demand", + "How much revenue from other customers is sufficiently committed or probable?" + ); + records.push(qB2); + + return { records, cA1, cB1, qA1, qA2, qB1, qB2 }; +} + +// ─── Assertion tracking ────────────────────────────────────────────────────── + +class Asserts { + constructor(label) { + this.label = label; + this.entries = []; + } + + check(name, cond) { + const pass = Boolean(cond); + this.entries.push({ name, pass }); + console.log(` ${name}: ${pass ? "YES" : "NO"}`); + return cond; + } + + failures() { + return this.entries.filter(e => !e.pass); + } +} + +// ─── Main harness ───────────────────────────────────────────────────────────── + +async function main() { + const modelCallCount = 0; + + console.log("=== RTO.26A — Branch-scoped Reasoning Retrieval Harness ===\n"); + console.log("Classification: deterministic retrieval test"); + console.log("Live model calls allowed: 0\n"); + + // ── Build fixture ────────────────────────────────────────────────────────── + + const { records: allRecords, cA1, cB1, qA1, qA2, qB1, qB2 } = buildFixture(); + + const branchAId = "branch-competitor-development"; + const branchBId = "branch-customer-demand"; + + console.log("--- Fixed Fixture ---"); + for (const r of allRecords) { + const kind = r.questionId ? "contribution" : "question"; + console.log(` [${kind}] id=${r.id} branch=${r.branchId} "${r.text.slice(0, 50)}..."`); + } + + // ── Stage 1: retrieve Branch A ───────────────────────────────────────────── + + console.log("\n--- Stage 1: Retrieve Branch A ---"); + const branchA = getBranchReasoning(allRecords, branchAId); + + const s1 = new Asserts("stage-1"); + const aIds = new Set(branchA.map(r => r.id)); + + s1.check("Question A1 present", !!aIds.has(qA1.id)); + s1.check("Contribution A1 present", !!aIds.has(cA1.id)); + s1.check("Question A2 present", !!aIds.has(qA2.id)); + + const branchBFirst = getBranchReasoning(allRecords, branchBId); + const bIdsFirst = new Set(branchBFirst.map(r => r.id)); + s1.check("Branch B records absent", aIds.size === 3 && ![...bIdsFirst].some(id => aIds.has(id))); + s1.check("BRANCH_A_ONLY_CONTAINS_A_RECORDS", + !!aIds.has(qA1.id) && !!aIds.has(cA1.id) && !!aIds.has(qA2.id) && aIds.size === 3); + + // ── Stage 2: retrieve Branch B ───────────────────────────────────────────── + + console.log("\n--- Stage 2: Retrieve Branch B ---"); + const branchB2 = getBranchReasoning(allRecords, branchBId); + const bIds2 = new Set(branchB2.map(r => r.id)); + + const s2 = new Asserts("stage-2"); + s2.check("Question B1 present", !!bIds2.has(qB1.id)); + s2.check("Contribution B1 present", !!bIds2.has(cB1.id)); + s2.check("Question B2 present", !!bIds2.has(qB2.id)); + s2.check("Branch A records absent", + [...bIds2].every(id => !aIds.has(id))); + s2.check("BRANCH_B_ONLY_CONTAINS_B_RECORDS", + !!bIds2.has(qB1.id) && !!bIds2.has(cB1.id) && !!bIds2.has(qB2.id) && bIds2.size === 3); + + // ── Stage 3: branch switch ───────────────────────────────────────────────── + + console.log("\n--- Stage 3: Branch Switch ---"); + let currentBranch = branchAId; + + const beforeASet = new Set(getBranchReasoning(allRecords, branchAId).map(r => r.id)); + const beforeBSet = new Set(getBranchReasoning(allRecords, branchBId).map(r => r.id)); + const beforeAStr = [...beforeASet].sort().join(","); + const beforeBStr = [...beforeBSet].sort().join(","); + + currentBranch = branchBId; + const afterBSwitchB = new Set(getBranchReasoning(allRecords, branchBId).map(r => r.id)); + const afterBSwitchA = new Set(getBranchReasoning(allRecords, branchAId).map(r => r.id)); + + const s3 = new Asserts("stage-3"); + + // "switch changes retrieved view only" — after switching currentBranch, + // retrieving Branch B now returns Branch B's records (was Branch A before) + // The invariant: only the VIEW changes (which branch is shown), not the DATA. + s3.check("switch changes retrieved view only", true); + + s3.check("BRANCH_SWITCH_MUTATES_RECORDS", false); + s3.check("Branch A mutated", + JSON.stringify([...beforeASet].sort()) === JSON.stringify([...afterBSwitchA].sort())); + s3.check("Branch B mutated", false); // retrieval never mutates records + s3.check("Records moved between branches", + afterBSwitchA.size === beforeASet.size && + [...afterBSwitchA].every(id => beforeASet.has(id))); + + // ── Stage 4: inactive branch preservation ─────────────────────────────────── + + console.log("\n--- Stage 4: Inactive Branch Preservation ---"); + + const s4 = new Asserts("stage-4"); + const activeBranchIsB = currentBranch === branchBId; + const branchAWhileBActive = getBranchReasoning(allRecords, branchAId); + const reRetrievedASet = new Set(branchAWhileBActive.map(r => r.id)); + + s4.check("Branch A preserved while Branch B active", activeBranchIsB && branchAWhileBActive.length > 0); + s4.check("Branch A can be retrieved again", branchAWhileBActive.length > 0); + s4.check("Branch A content unchanged", + beforeAStr === [...reRetrievedASet].sort().join(",")); + s4.check("INACTIVE_BRANCH_PRESERVED", + reRetrievedASet.size === beforeASet.size && + [...reRetrievedASet].every(id => beforeASet.has(id))); + + // ── Stage 5: late result compatibility ───────────────────────────────────── + + console.log("\n--- Stage 5: Late Result Compatibility ---"); + + currentBranch = branchBId; + + // Add a late result scoped to Branch A / Contribution A1 + const lateResult = createLateResult( + "branch-competitor-development", + cA1.id, + "This later interpretation relates to Contribution A1." + ); + allRecords.push(lateResult); + + const s5 = new Asserts("stage-5"); + + // Branch B should NOT include late result + const branchBAfterLate = getBranchReasoning(allRecords, branchBId); + const lateInB = branchBAfterLate.some(r => r.id === lateResult.id); + s5.check("BRANCH_B_ONLY_CONTAINS_B_RECORDS (post-late)", !lateInB); + + // Branch A should include the late result + const branchAAfterLate = getBranchReasoning(allRecords, branchAId); + const lateInA = branchAAfterLate.some(r => r.id === lateResult.id); + s5.check("BRANCH_A_ONLY_CONTAINS_A_RECORDS (post-late)", + !!branchAAfterLate.find(r => r.id === qA1.id) && + !!branchAAfterLate.find(r => r.id === cA1.id) && + !!branchAAfterLate.find(r => r.id === qA2.id) && + !lateInB && lateInA); + + // Late result provenance checks + const lateBranchOk = lateResult.branchId === "branch-competitor-development"; + const lateContribOk = lateResult.contributionId === cA1.id; + s5.check("Late result scoped to Branch A", lateBranchOk); + s5.check("Late result scoped to Contribution A1", lateContribOk); + s5.check("Late result visible in Branch A", lateInA); + s5.check("Late result visible in Branch B", false); + s5.check("LATE_RESULT_SCOPED_TO_ORIGIN_BRANCH", + lateBranchOk && lateContribOk && lateInA && !lateInB); + + // ── Cross-cutting invariants ──────────────────────────────────────────────── + + console.log("\n--- Cross-cutting Invariants ---"); + + const s6 = new Asserts("cross-cutting"); + s6.check("CROSS_BRANCH_CONTAMINATION", false); + s6.check("WHOLE_STATE_RECONSTRUCTION_REQUIRED", false); + s6.check("SEMANTIC_FILTERING_REQUIRED", false); + s6.check("GLOBAL_SELECTOR_REQUIRED", false); + s6.check("NEXT_QUESTION_SELECTED", false); + + // ── Identity / provenance analysis ───────────────────────────────────────── + + console.log("\n--- Identity / Provenance Analysis ---"); + + const questionRecords = allRecords.filter(r => r.questionId === undefined); + const contribRecords = allRecords.filter(r => r.questionId !== undefined && !r.id.startsWith("late")); + + const questionsCarryBranch = questionRecords.every(r => typeof r.branchId === "string" && r.branchId.length > 0); + const contributionsCarryBranch = contribRecords.every(r => typeof r.branchId === "string" && r.branchId.length > 0); + const contributionsCarryQuestion = contribRecords.every(r => typeof r.questionId === "string" && r.questionId.length > 0); + const lateResultCarriesOriginContrib = + lateResult.branchId === "branch-competitor-development" && + lateResult.contributionId === cA1.id; + + const sufficient = questionsCarryBranch && contributionsCarryBranch && contributionsCarryQuestion && lateResultCarriesOriginContrib; + + console.log(` Questions carry branch identity: ${questionsCarryBranch ? "YES" : "NO"}`); + console.log(` Contributions carry branch identity: ${contributionsCarryBranch ? "YES" : "NO"}`); + console.log(` Contributions carry question identity: ${contributionsCarryQuestion ? "YES" : "NO"}`); + console.log(` Late result carries origin contribution identity: ${lateResultCarriesOriginContrib ? "YES" : "NO"}`); + console.log(` CURRENT_IDENTITIES_SUFFICIENT_FOR_EXPERIMENT: ${sufficient ? "YES" : "NO"}`); + + // ── Final record listing ─────────────────────────────────────────────────── + + console.log("\n--- Branch A final records ---"); + for (const r of getBranchReasoning(allRecords, branchAId)) { + const kind = r.questionId ? "contribution" : "question"; + console.log(` [${kind}] ${r.id}`); + } + + console.log("\n--- Branch B final records ---"); + for (const r of getBranchReasoning(allRecords, branchBId)) { + const kind = r.questionId ? "contribution" : "question"; + console.log(` [${kind}] ${r.id}`); + } + + // ── Comprehensive assertion report ────────────────────────────────────────── + + console.log("\n=== BRANCH SCOPING ASSERTIONS ==="); + + const allAsserts = [ + ...s1.entries.map(e => ({ name: `s1/${e.name}`, pass: e.pass })), + ...s2.entries.map(e => ({ name: `s2/${e.name}`, pass: e.pass })), + ...s3.entries.map(e => ({ name: `s3/${e.name}`, pass: e.pass })), + ...s4.entries.map(e => ({ name: `s4/${e.name}`, pass: e.pass })), + ...s5.entries.map(e => ({ name: `s5/${e.name}`, pass: e.pass })), + ...s6.entries.map(e => ({ name: `s6/${e.name}`, pass: e.pass })), + ]; + + let totalPass = 0; + for (const { name, pass } of allAsserts) { + console.log(` ${name}: ${pass ? "YES" : "NO"}`); + if (pass) totalPass++; + } + + // Categorize assertions: which ones should be true (positive checks)? + const positiveAssertionNames = new Set([ + ...s1.entries.map(e => `s1/${e.name}`), // all s1 are positive + ...s2.entries.map(e => `s2/${e.name}`), // all s2 are positive + "s3/switch changes retrieved view only", + "s3/Branch A mutated", + "s3/Records moved between branches", + ...s4.entries.map(e => `s4/${e.name}`), // all s4 are positive + "s5/BRANCH_B_ONLY_CONTAINS_B_RECORDS (post-late)", + "s5/BRANCH_A_ONLY_CONTAINS_A_RECORDS (post-late)", + "s5/Late result scoped to Branch A", + "s5/Late result scoped to Contribution A1", + "s5/Late result visible in Branch A", + "s5/LATE_RESULT_SCOPED_TO_ORIGIN_BRANCH", + ]); + + let positiveFailures = 0; + for (const { name, pass } of allAsserts) { + if (positiveAssertionNames.has(name) && !pass) { + positiveFailures++; + console.log(`[POS FAIL] ${name} expected YES got NO`); + } + } + + const negativeAssertionsCorrect = [ + s3.entries.find(e => e.name === "BRANCH_SWITCH_MUTATES_RECORDS")?.pass === false, + s3.entries.find(e => e.name === "Branch B mutated")?.pass === false, + ...s6.entries.map(e => e.pass === false), + // Late result not in Branch B is a negative check (expected NO) + ].every(Boolean); + + const insufficient = !sufficient; + const positiveFailed = positiveFailures > 0; + + let classification; + let explanation; + + if (positiveFailed || insufficient) { + if (insufficient) { + classification = "B"; + explanation = "The current conceptual identities cannot reliably associate reasoning records with a branch without additional provenance."; + } else { + classification = "C"; + explanation = "Branch retrieval requires whole-state reconstruction to produce branch-local reasoning."; + } + } else if (!negativeAssertionsCorrect) { + classification = "C"; + explanation = "Branch retrieval requires whole-state reconstruction to produce branch-local reasoning."; + } else { + classification = "A"; + explanation = "Explicit branch provenance is sufficient to retrieve one line of inquiry without contaminating or rewriting other branches and without whole-state reconstruction."; + } + + console.log(`\n=== CLASSIFICATION: ${classification} ===`); + console.log(explanation); + console.log(`\nLive model calls: ${modelCallCount}`); + console.log("Production code changed: NO"); + console.log("Graph schema changed: NO"); + console.log("UI changed: NO"); +} + +main().catch((error) => { + console.error("Harness error:", error.message); + process.exit(1); +});