diff --git a/lib/fixtures/rto26b-branch-scoped.mjs b/lib/fixtures/rto26b-branch-scoped.mjs new file mode 100644 index 0000000..d759db3 --- /dev/null +++ b/lib/fixtures/rto26b-branch-scoped.mjs @@ -0,0 +1,231 @@ +/** + * RTO.26B — Branch-scoped experimental fixture + * + * Replicates the RTO.26A branch record inside the UI layer. + * Membership is determined by explicit `branchId` provenance only. + * + * No semantic filtering, keyword matching, or whole-state reconstruction. + */ + +// ── Deterministic ID generator (mirrors RTO.26A harness) ─────────────── + +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")}`; +} + +// ── Record model (explicit provenance via `branchId`) ───────────────── + +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, + }; +} + +// ── Fixed fixture data (mirrors RTO.26A stages 1–5) ─────────────────── + +function buildBranchScopedFixture() { + 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); + + // Late result scoped to Branch A / Contribution A1 (stage 5) + const lateResultA1 = createLateResult( + "branch-competitor-development", + cA1.id, + "This later interpretation relates to Contribution A1." + ); + records.push(lateResultA1); + + return { + // Records keyed by branch provenance + allRecords: records, + questions: { + a1: qA1, a2: qA2, b1: qB1, b2: qB2, + }, + contributions: { a1: cA1, b1: cB1 }, + lateResults: { a1: lateResultA1 }, + }; +} + +// ── Branch-scoped retrieval (explicit provenance only) ───────────────── + +/** + * getBranchReasoning(records, branchId) + * Returns ONLY records whose `branchId` matches. No semantic filtering. + */ +function getBranchReasoning(allRecords, branchId) { + return allRecords.filter(r => r.branchId === branchId); +} + +// ── Branch-to-label mapping (UI layer only — not provenance logic) ───── + +const BRANCH_META = { + "branch-competitor-development": { + id: "branch-a", + label: "Competitor development", + origin: "Whether competitors are developing similar products and when they might release them", + }, + "branch-customer-demand": { + id: "branch-b", + label: "Customer demand", + origin: "How many customers will buy the product, and what revenue that represents", + }, +}; + +// ── Exported interface for UI consumption ──────────────────────────────── + +export function useBranchScopedFixture() { + const fixture = buildBranchScopedFixture(); + const allRecords = fixture.allRecords; + + return { + /** Return branch-local question records as node-like objects. */ + getBranchQuestions(branchId) { + const recs = getBranchReasoning(allRecords, branchId); + return recs + .filter(r => !r.questionId && !r.id.startsWith("late")) + .map(q => ({ + id: q.id, + label: q.text, + kind: "unknown", + status: "unresolved", + _isFixtureQuestion: true, + contributions: q.contributions || [], + })); + }, + + /** Return branch-local contribution records scoped to active branch. */ + getBranchContributions(branchId) { + const recs = getBranchReasoning(allRecords, branchId); + return recs.filter(r => r.questionId && !r.id.startsWith("late")); + }, + + /** Check if a late result exists for the given contribution. */ + hasLateResult(contributionId) { + return Object.values(fixture.lateResults).some(lr => lr.contributionId === contributionId); + }, + + /** Return late results scoped to a specific branch (for display). */ + getBranchLateResults(branchId) { + const recs = getBranchReasoning(allRecords, branchId); + return recs.filter(r => r.id.startsWith("late")); + }, + + /** Branch metadata for UI labels. */ + getBranchMeta(id) { + // Reverse lookup: UI id → provenance branchId → meta + const entry = Object.entries(BRANCH_META).find( + ([, m]) => m.id === id + ); + return entry ? { ...entry[1] } : null; + }, + + /** Return the fixed two-branch list for the switcher. */ + getBranches() { + return Object.values(BRANCH_META).map(m => ({ ...m })); + }, + + /** Return all records (for verification / debugging). */ + getAllRecords() { + return allRecords; + }, + + /** Verify that a record's provenance matches its branch. */ + verifyProvenance(record, expectedBranchId) { + return record?.branchId === expectedBranchId; + }, + + // ── RTO.26A cross-cutting invariants (deterministic checks) ────── + + _crossCuttingInvariants() { + const branchAIds = new Set(getBranchReasoning(allRecords, "branch-competitor-development").map(r => r.id)); + const branchBIds = new Set(getBranchReasoning(allRecords, "branch-customer-demand").map(r => r.id)); + + return { + SEMANTIC_FILTERING_REQUIRED: false, + KEYWORD_MATCHING_REQUIRED: false, + WHOLE_STATE_RECONSTRUCTION_REQUIRED: false, + GLOBAL_SELECTOR_REQUIRED: false, + CROSS_BRANCH_CONTAMINATION: [...branchAIds].some(id => branchBIds.has(id)), + NEXT_QUESTION_SELECTED: false, + }; + }, + + // Expose fixture for inspection (RTO.26A stages 1–5) + _fixtureRecords: allRecords, + _qA1: fixture.questions.a1, + _qA2: fixture.questions.a2, + _qB1: fixture.questions.b1, + _qB2: fixture.questions.b2, + _cA1: fixture.contributions.a1, + _cB1: fixture.contributions.b1, + _lateResultA1: fixture.lateResults.a1, + }; +} + +export { buildBranchScopedFixture };