test(experiment): checkpoint branch scoped reasoning retrieval
This commit is contained in:
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user