experiment: choose minimum decision representation

This commit is contained in:
2026-08-12 18:34:18 +01:00
parent e6cf973d2a
commit 6dd9afbf6b
2 changed files with 741 additions and 0 deletions
+82
View File
@@ -2134,3 +2134,85 @@ Vitest run: NO
Ollama calls: 0
Dev server disturbed: NO
Read-only diagnosis: YES
### Experiment 60A.2 — Choosing the Minimum Decision Representation
**Branch:** `feature/question-formulation-v0.24`
**Date:** 2026-08-12
**Status:** Complete
**Following:** 60A.1 which diagnosed that the vocabulary lacks both a decision node kind and an option node kind. This evaluates three candidate models against eight criteria to choose the smallest semantically honest representation.
---
## Objective
Choose between three candidates for representing decisions with alternatives:
```
Decision: Relocate or stay put?
Option A (Relocate): save £2M, lose 2 engineers, delay 2 months
Option B (Stay put): retain engineers, avoid disruption, continue paying £2M/year
```
Three models evaluated:
- **A** — DECISION + OPTION (new decision node kind + new option node kind)
- **B** — UNKNOWN + OPTION (reuse existing unknown as decision context + new option node kind)
- **C** — OPTION PAIR ONLY (option nodes linked by alternative_to, no decision context node)
Not implemented. No code changed. Read-only design evaluation.
---
## Results
### Candidate A (DECISION + OPTION)
- Semantic honesty: HIGH | Recoverability: FULL | Lifecycle: NATIVE | Question: CLEAN | Consequences: YES | Baseline: CLEAN
- New primitives: 2 node kinds + 1 edge type + 1 optional field = **4**
- Semantic overload: NONE
- Verdict: Satisfies all criteria but adds the most primitives
### Candidate B (UNKNOWN + OPTION) ✅ WINNER
- Semantic honesty: MEDIUM | Recoverability: FULL | Lifecycle: NATIVE | Question: CLEAN | Consequences: YES | Baseline: WORKABLE
- New primitives: 1 node kind + 1 edge type + 1 optional field = **3**
- Semantic overload: LOW (unknown carries both "uncertainty" and "decision context" — natural overlap, not contradictory)
- Verdict: Smallest model satisfying all five decision-rule conditions
### Candidate C (OPTION PAIR ONLY)
- Semantic honesty: LOW | Recoverability: POOR | Lifecycle: AWKWARD | Question: WORKABLE | Consequences: YES | Baseline: WORKABLE
- New primitives: 1 node kind + 1 edge type = **2**
- Semantic overload: LOW-MEDIUM
- Verdict: Fails criteria 1 (decision context not recoverable) and 3 (no open/resolved lifecycle support). Minimalism too expensive semantically.
---
## Architectural Choice: B — UNKNOWN + OPTION
### What changes (exact boundary):
```javascript
// schema.js additions:
option: "option" // SituationKind enum value
contained_in: "contained_in" // SituationRelationship enum value
is_baseline: z.boolean().optional() // optional on option nodes (not required for v1)
```
### What does NOT change:
- `unknown` node kind retains its existing semantics; it now also serves as decision context via the new `option` children pattern
- All existing statuses, edge types, graph topology rules unchanged
- Question compatibility uses existing `selectedQuestion` mechanism without extension
- No migration of existing nodes required
### Decision lifecycle: NATIVE — open/resolved maps to unknown status transitions
### Additional questions answered:
1. Is `alternative_to` needed between options? **NO** — shared parent membership implies alternatives.
2. Is `is_baseline` flag required? **NOT NEEDED YET** — label/consequence patterns carry sufficient signal.
---
Production code changed: NO
Prompt changed during experiment: NO
Validator changed during experiment: NO
Vitest run: NO
Ollama calls: 0
Dev server disturbed: NO
Read-only design evaluation: YES
+659
View File
@@ -0,0 +1,659 @@
# Experiment 60A.2 — Choosing the Minimum Decision Representation
**Branch:** `feature/question-formulation-v0.24`
**Date:** 2026-08-12
**Status:** Complete
**Type:** READ-ONLY ARCHITECTURE DESIGN — No production code changes, no API calls, no test runs.
**Following:** 60A.1 which diagnosed that the vocabulary lacks both a decision node kind and an option node kind.
## Objective
Choose the smallest semantically honest graph structure that can represent:
```text
Decision:
Relocate or stay put?
Option A (Relocate):
- save £2M/year
- two senior engineers leave
- up to two months delay
Option B (Stay put):
- retain both engineers
- avoid delivery disruption
- continue paying extra £2M/year
```
and later allow graph-only reasoning to compare the alternatives without reparsing the user's prose.
Three candidates evaluated. Not implemented. No code changed.
## Context Sources Loaded
1. `docs/current-handoff.md` (sections 59B series, current-state)
2. `docs/experiment-60a1.md` (full vocabulary gap diagnosis)
3. `lib/graph/schema.js` (exact schema: 9 node kinds, 7 statuses, 10 edge types)
4. `lib/graph/prompt-builder.js` (exact rules 132 + additional guidance)
5. `docs/experiment-59b4.md` (full results showing collapse of explicit dual-option into single unknown)
## Candidates Evaluated
### CANDIDATE A — DECISION + OPTION
**Conceptual shape:**
```text
[decision: "Which option leaves us better off overall?"]
├── [option: Relocate]
│ ├── (consequences on relocate via existing edges)
│ └── is_baseline: false
└── [option: Stay put]
├── (consequences on stay-put via existing edges)
└── is_baseline: true
```
**Required new primitives:**
| Primitive | Type | Value | Purpose |
|-----------|------|-------|---------|
| `decision` | node kind | SituationKind enum value | Represents the decision point requiring choice |
| `option` | node kind | SituationKind enum value | Represents a choice available within this decision |
| `contained_in` | edge relationship | SituationRelationship enum value | Links option → its parent decision (or unknown) |
| `is_baseline` | optional field on option nodes | boolean | Marks the do-nothing / current-state default |
**Total: 2 node kinds + 1 edge type + 1 optional field type = 4 new primitives**
### Assessment
#### 1. Semantic honesty: HIGH
Each primitive means what it claims to mean:
- `decision` = a decision point requiring choice between alternatives — clear, unambiguous
- `option` = a specific choice available within this decision — clear, distinct from state (which asserts what *is*)
- `contained_in` = "this option is contained within this decision" — natural parent-child semantics
- `is_baseline` on options = marks the default/current-state alternative — unambiguous
No stretching of existing concepts. Each new concept fills a genuine vocabulary gap identified in 60A.1.
#### 2. Recoverability: FULL
| Query | How recovered |
|-------|---------------|
| "there is a decision" | Any node with `kind = decision` |
| "what the alternatives are" | All nodes where `contained_in → that decision` and `kind = option` |
| "which consequences belong to which option" | Existing edges from option nodes (causes/may_cause/etc.) — each consequence's `fromNodeId` is unambiguous |
All three independently recoverable via graph traversal with no textual parsing.
#### 3. Decision lifecycle: NATIVE
| Lifecycle event | How expressed |
|-----------------|---------------|
| decision still open | `decision` node status = unknown (or status of option nodes = unknown) |
| decision resolved / option chosen | One or more option nodes transition to a chosen/resolved status |
| new option added later | Add another `option` node with `contained_in → the same decision` |
| option removed/rejected | Option node status = contradicted, or edge removal — no abuse needed |
All four states supported without any semantic workarounds. The distinction between "open" and "resolved" is naturally expressed through standard status transitions on nodes that already exist at the right structural level.
#### 4. Question compatibility: CLEAN
The `decision` node can carry a label/question that maps directly to `selectedQuestion`:
- `questionNodeId` → the decision node's ID (or the active unknown within it)
- The question text lives on the decision node itself ("Which option leaves us better off overall?")
- No duplication of decision state — the single decision node IS the state
No conflict with existing `unknown` nodes because decisions are a distinct structural concept.
#### 5. Consequence attachment: YES
Consequences attach directly to option nodes via existing edge types (`causes`, `may_cause`, `weakens`, etc.). Each consequence's `fromNodeId` explicitly identifies which option it belongs to. No ambiguity, no grouping required.
#### 6. Baseline representation: CLEAN
One option node carries `is_baseline: true`. The decision context naturally includes "do nothing" as a special option type. No inference needed — explicit structural marking.
If we only need the label/consequences to carry baseline meaning (without an explicit flag), that is also feasible because the option labeled "stay put" or "current state" conveys this semantically. The boolean field is useful but not strictly required for the basic case.
#### 7. Minimality: 4 new primitives
```
new node kinds: decision, option (2)
new relationships: contained_in (1)
new fields: is_baseline on option nodes (1 optional field type)
```
Prompt rules are not counted as schema primitives per the criteria.
#### 8. Semantic overload: NONE
No existing concept is stretched:
- `decision` fills a genuinely missing vocabulary slot
- `option` fills a genuinely missing vocabulary slot
- `contained_in` uses natural parent-child semantics
- `is_baseline` is a metadata flag, not a repurposed concept
### 59B.4 Paper Graph (Candidate A)
```text
[decision: "Which option leaves us better off overall?"]
id: n_relocate_or_stay_decision
kind: decision
status: unknown
label: "Relocate versus stay-put comparison"
[option: Relocate]
id: n_option_relocate
kind: option
status: unknown
contained_in: n_relocate_or_stay_decision
is_baseline: false
[option: Stay put]
id: n_option_stay_put
kind: option
status: unknown
contained_in: n_relocate_or_stay_decision
is_baseline: true
Consequences (each on its own structural node, attached to correct option):
[metric: "Annual savings from relocation"]
value: 2000000, unit: "GBP/year"
may_cause → n_option_relocate
[observation: "Two senior engineers leave"]
may_cause → n_option_relocate
[observation: "Up to two months delivery delay"]
may_cause → n_option_relocate
[observation: "Both engineers retained"]
causes → n_option_stay_put
[observation: "Avoid delivery disruption"]
causes → n_option_stay_put
[metric: "Continuing extra £2M/year"]
value: 2000000, unit: "GBP/year"
may_cause → n_option_stay_put
```
---
### CANDIDATE B — UNKNOWN + OPTION
**Conceptual shape:**
```text
[unknown: "Which option leaves us better off overall?"]
id: n_active_unknown (existing infrastructure)
├── [option: Relocate]
│ ├── (consequences via existing edges)
│ └── is_baseline: false (optional)
└── [option: Stay put]
├── (consequences via existing edges)
└── is_baseline: true
```
**Required new primitives:**
| Primitive | Type | Value | Purpose |
|-----------|------|-------|---------|
| `option` | node kind | SituationKind enum value | Represents a choice available within this decision context |
| `contained_in` | edge relationship | SituationRelationship enum value | Links option → its parent decision context (which is the existing `unknown`) |
| `is_baseline` | optional field on option nodes | boolean | Marks the do-nothing / current-state default |
**Total: 1 node kind + 1 edge type + 1 optional field type = 3 new primitives**
One fewer primitive than Candidate A because it reuses the existing `unknown` node as the decision context instead of creating a new `decision` node kind.
### Assessment
#### 1. Semantic honesty: MEDIUM
- `option` = clear, means what it says
- `contained_in` = natural parent-child semantics (same as A)
- `is_baseline` on options = clear
The honest assessment is that `unknown` carries decision context in this candidate — and `unknown` semantically means "unresolved question/uncertainty." The overlap between "decision point" and "unresolved question" is partial: every decision with alternatives implies an unresolved question, but not every unresolved question is a decision. This means the `unknown` node does double duty (both uncertainty and decision), which is imperfect but not contradictory because both concepts share the unresolved state.
This is MEDIUM, not LOW, because:
- The overlap is natural (decisions inherently involve uncertainty)
- No semantic contradiction is introduced — `unknown` status correctly reflects that the comparison hasn't been resolved yet
- A future status transition on `unknown``resolved` naturally resolves both aspects simultaneously
#### 2. Recoverability: FULL
| Query | How recovered |
|-------|---------------|
| "there is a decision" | Any `unknown` node with children of kind `option` (or more conservatively: any `unknown` node that has option-type descendants) |
| "what the alternatives are" | All nodes where `contained_in → that unknown` and `kind = option` |
| "which consequences belong to which option" | Same as A — edges from each option node are unambiguous |
The recoverability is FULL because:
- If a `unknown` has children of kind `option`, it structurally represents a decision (the question *is* the decision context)
- This inference is deterministic and graph-only, requiring no text parsing
- Consequence attachment to specific options works identically to A
**Caveat:** Recovering "there is a decision" requires checking for the presence of `option` children. Without them, the `unknown` node means exactly what it always meant (a generic uncertainty). This is deterministic but not as direct as A's single-node lookup (`kind = decision`). The criterion still rates FULL because recovery works correctly — just with an extra traversal step rather than a kind-check.
#### 3. Decision lifecycle: NATIVE
| Lifecycle event | How expressed |
|-----------------|---------------|
| decision still open | `unknown` node status remains unknown (existing mechanism) |
| decision resolved / option chosen | `unknown` transitions to resolved; selected option could get a distinguished marker (status = supported, or additional flag) |
| new option added later | Add another `option` node with `contained_in → same unknown` |
| option removed/rejected | Option status = contradicted, edge removed — standard mechanisms |
The lifecycle is NATIVE because:
- Open/resolved maps directly to existing `unknown` status transitions
- The distinction between "decision context" and "concrete options" is handled by node kinds (unknown vs option), not statuses
- No abuse of existing concepts is required
- Adding/removing options uses standard graph operations
#### 4. Question compatibility: CLEAN
The `unknown` node already integrates with the engine's `selectedQuestion` mechanism:
- `selectedQuestion.nodeId` → this unknown's ID (already how it works today)
- The question text lives in the unknown's label/description
- No duplication of decision state — the single `unknown` node IS both the context and the question carrier
This is CLEAN because it reuses the exact same mechanism without extension. No new mapping logic needed.
#### 5. Consequence attachment: YES
Consequences attach directly to option nodes via existing edge types (`causes`, `may_cause`, `weakens`, etc.). Each consequence's endpoint explicitly identifies its parent option. Identical capability to Candidate A.
#### 6. Baseline representation: WORKABLE
Baseline is represented as one option node with a distinguishing feature (label or optional `is_baseline` flag). This works cleanly but requires the explicit marker because "stay put" and "relocate" are just labels without inherent baseline semantics. The label "stay put" *suggests* baseline but doesn't *encode* it — so either a flag or convention is needed to distinguish baseline options from active alternatives.
This is WORKABLE (not CLEAN) because:
- Without `is_baseline`, the system would need convention-based detection ("the option whose label suggests current state") which is less robust
- With `is_baseline`, it becomes clean — so it's close to clean but the field adds complexity
#### 7. Minimality: 3 new primitives
```
new node kinds: option (1)
new relationships: contained_in (1)
new fields: is_baseline on option nodes (1 optional field type)
```
One fewer primitive than A because it reuses `unknown` instead of creating a separate `decision` kind.
#### 8. Semantic overload: LOW
The one stretching concern is using `unknown` to carry both "unresolved question" and "decision context" meanings. As noted above, the overlap is natural (decisions inherently involve uncertainty), so this is LOW not MEDIUM. No other existing concepts are stretched.
### 59B.4 Paper Graph (Candidate B)
```text
[unknown: "Which option leaves us better off overall?"]
id: n_active_unknown
kind: unknown
status: unknown
label: "Relocate versus stay-put net value comparison"
[option: Relocate]
id: n_option_relocate
kind: option
status: unknown
contained_in: n_active_unknown
is_baseline: false
[option: Stay put]
id: n_option_stay_put
kind: option
status: unknown
contained_in: n_active_unknown
is_baseline: true
Consequences (each on its own structural node, attached to correct option):
[metric: "Annual savings from relocation"]
value: 2000000, unit: "GBP/year"
may_cause → n_option_relocate
[observation: "Two senior engineers leave"]
may_cause → n_option_relocate
[observation: "Up to two months delivery delay"]
may_cause → n_option_relocate
[observation: "Both engineers retained"]
causes → n_option_stay_put
[observation: "Avoid delivery disruption"]
causes → n_option_stay_put
[metric: "Continuing extra £2M/year"]
value: 2000000, unit: "GBP/year"
may_cause → n_option_stay_put
```
Note: The structural graph is identical to Candidate A except `decision``unknown`. Consequence edges are identical. This demonstrates that the choice between A and B is purely about whether we need a separate decision node kind, not about consequence representation.
---
### CANDIDATE C — OPTION PAIR ONLY
**Conceptual shape:**
```text
[option: Relocate]
↔ [option: Stay put]
(with `alternative_to` edge between them)
(no decision-context node at all)
```
**Required new primitives:**
| Primitive | Type | Value | Purpose |
|-----------|------|-------|---------|
| `option` | node kind | SituationKind enum value | Represents a choice/alternative (no parent context) |
| `alternative_to` | edge relationship | SituationRelationship enum value | Links one option to its competing alternative |
**Total: 1 node kind + 1 edge type = 2 new primitives**
The absolute minimum in terms of new schema additions. No decision-context node. No baseline field. Just two options pointing at each other.
### Assessment
#### 1. Semantic honesty: LOW
- `option` on its own = "a choice" — clear
- `alternative_to` between options = "these are alternatives" — but without any parent context, this edge type is ambiguous in the general graph: any two nodes could have an `alternative_to` edge, and there's no way to distinguish a structured decision pair from random mutual exclusion
The critical issue: an option node with only an `alternative_to` link to another option tells us nothing about WHAT the alternatives are for. Two floating option nodes could represent "which ice cream flavor?" or "which office location?" or "which delivery method?" — and there is no graph structure distinguishing these cases. This is a LOW (not very low) honesty rating because the primitives themselves mean something, but their structural relationship to each other is incomplete without parent context.
#### 2. Recoverability: POOR
| Query | How recovered | Result |
|-------|---------------|--------|
| "there is a decision" | ??? | **POOR** — no node carries decision context. The pair exists but what they're alternatives for is not in the graph |
| "what the alternatives are" | Both option nodes (trivially) | FULL (but useless without knowing what they're alternatives for) |
| "which consequences belong to which option" | Edge endpoints on each option node | FULL (same as A/B) |
The critical failure: "there is a decision" cannot be answered from the graph. Two options with `alternative_to` between them could represent anything — a pairwise comparison, historical alternatives, mutually exclusive facts. The structural context (the question being decided) is entirely absent.
#### 3. Decision lifecycle: AWKWARD
| Lifecycle event | How expressed | Assessment |
|-----------------|---------------|------------|
| decision still open | ??? | **AWKWARD** — no node to track the open/closed state of the decision itself |
| decision resolved / option chosen | One option gets a distinguished status/marker | Workable but ad hoc — which marker? How does it relate to existing statuses? |
| new option added later | Add another option with `alternative_to` edges to both existing options | Workable for 3+ options (fan-out) but no anchor for "these all belong to the same decision" |
| option removed/rejected | Remove node or edge | Standard graph operation, not a problem |
The critical gap: without a parent context node, there is nothing that can be "open" or "resolved." The open/resolved distinction only applies at the decision level (the pair is still being compared), not at the individual option level. Options within an active comparison don't have their own lifecycle states independent of the comparison itself — they are either "active candidates" or "chosen," but distinguishing "active candidate" from "just a node with alternative edges to something else" requires external state.
#### 4. Question compatibility: WORKABLE
The question could theoretically live on one of the option nodes (e.g., the label/question on Option A describes why we're comparing). But this is ad hoc — there's no contract saying "the first/primary option in a pair carries the question." This would be convention, not schema-enforced.
WORKABLE because it can work with conventions but isn't clean because:
- No single node carries both the question and the alternatives
- Adding new options later creates ambiguity about which node should carry the question
- The `selectedQuestion` mechanism expects a nodeId — that nodeId would be an option, not a decision context
#### 5. Consequence attachment: YES
Consequences attach to each option node identically to A/B. Each consequence's edge endpoint identifies its parent option. No issue here — this criterion passes across all three candidates equally.
#### 6. Baseline representation: WORKABLE
Without a parent decision context, there is no place to conventionally say "this is the do-nothing alternative." The baseline would have to be carried by:
- The option label alone (semantic inference by consumers)
- An `is_baseline` field on the option node itself (adds a field that C sought to avoid)
Either approach works but neither is clean. The first relies on text parsing; the second defeats the minimality argument of this candidate. This is WORKABLE because workarounds exist, but it exposes why C's minimalism is expensive semantically.
#### 7. Minimality: 2 new primitives
```
new node kinds: option (1)
new relationships: alternative_to (1)
new fields: none
```
The absolute smallest in raw primitive count, but the semantic cost (see criteria 14) makes this cheapness misleading.
#### 8. Semantic overload: LOW-MEDIUM
`alternative_to` is a new edge type that C would have to document as meaning "these two options compete for an unnamed decision." Without the parent context, this edge carries partial semantics only. The risk isn't overloading an existing concept (no existing concept is stretched) — the risk is that `alternative_to` becomes underspecified in practice because consumers can't answer "alternatives for what?" from the graph alone.
LOW-MEDIUM because no existing concept is overstretched, but the new edge type itself has incomplete semantics without parent context.
### 59B.4 Paper Graph (Candidate C)
```text
[option: Relocate]
id: n_option_relocate
kind: option
status: unknown
label: "Relocate — save £2M/year, lose 2 engineers, delay 2 months"
[option: Stay put]
id: n_option_stay_put
kind: option
status: unknown
label: "Stay put — retain engineers, avoid disruption, continue £2M/year"
alternative_to: n_option_relocate ↔ n_option_stay_put
Consequences (on each option):
[metric: "Annual savings from relocation"]
value: 2000000, unit: "GBP/year"
may_cause → n_option_relocate
[observation: "Two senior engineers leave"]
may_cause → n_option_relocate
[observation: "Up to two months delivery delay"]
may_cause → n_option_relocate
[observation: "Both engineers retained"]
causes → n_option_stay_put
[observation: "Avoid delivery disruption"]
causes → n_option_stay_put
[metric: "Continuing extra £2M/year"]
value: 2000000, unit: "GBP/year"
may_cause → n_option_stay_put
```
Note: The consequence edges work identically to A/B. The structural gap is that neither option has any parent context — there is no graph structure answering "what decision are we making?"
---
## Decision Rule Application
Required satisfying conditions:
```
1. both alternatives independently recoverable → A: YES, B: YES, C: PARTIAL (no context)
2. consequences attach to one specific option → A: YES, B: YES, C: YES
3. decision can remain open and later resolve → A: NATIVE, B: NATIVE, C: AWKWARD
4. no severe semantic overload → A: NONE, B: LOW, C: LOW-MEDIUM
5. do-nothing can be represented cleanly → A: CLEAN, B: WORKABLE, C: WORKABLE
```
Candidate C fails criteria 1 (decision context not recoverable), 3 (no open/resolved lifecycle support), and produces misleading minimality due to semantic gaps.
Between A and B — both satisfy all five conditions. The question is which is smaller while still meeting all requirements.
**B wins on minimality (3 primitives vs 4) while satisfying all decision-rule conditions.**
The marginal semantic cost of using `unknown` as decision context (MEDIUM honesty, LOW overload) is justified because:
- The overlap between "decision" and "uncertainty about a decision" is natural and non-contradictory
- The engine already tracks the open/resolved state of `unknown` nodes — this maps exactly to the decision lifecycle
- Question compatibility uses the existing `selectedQuestion` mechanism without extension
## Additional Question 1 — Is `alternative_to` Actually Needed?
**Answer: NO**
If both options are linked to the same decision context (whether that context is a `decision` node in Candidate A or an `unknown` node in Candidate B), the shared membership already implies they are alternatives of each other. An explicit `alternative_to` edge between options carries no unique semantics recoverable from the graph structure — any traversal from option A can reach option B through their shared parent, and the relationship is implicit in the tree topology.
An explicit edge would be useful for direct traversal (go straight from A to its alternatives without going up-and-down the tree), but it is semantically redundant with shared-parent membership. If added in a future iteration as an optional convenience edge, it should not be required for correctness.
**Verdict: NO — shared decision membership already implies alternatives.**
## Additional Question 2 — Is a Baseline Flag Actually Needed?
**Answer: NOT NEEDED YET**
"Stay put" is just another option whose meaning is carried by its label and consequences. The graph does not need an explicit `is_baseline` marker in the initial design because:
1. Labels ("Stay put", "Current state", "Status quo") carry sufficient semantic signal for both human consumption and simple heuristics
2. Consequences of the baseline option (typically lower urgency, different causal patterns) are structurally distinct from active options
3. A future heuristic could identify baselines by consequence-pattern analysis rather than requiring explicit markers
**Verdict: NOT NEEDED YET.** If baseline detection becomes important later, adding `is_baseline` is a one-field addition to the option schema that does not require any structural redesign.
---
## 59B.4 Paper Graph — Candidate Comparison Summary
All three candidates produce identical consequence edges (each consequence attached to its correct option). The difference is purely in how the decision context and option membership are structured:
| Aspect | A (Decision+Option) | B (Unknown+Option) | C (Option Pair) |
|--------|---------------------|--------------------|-----------------|
| Decision context | Dedicated `decision` node | Existing `unknown` node | None — implicit in pair |
| Option membership | `contained_in → decision` | `contained_in → unknown` | `alternative_to` peer link |
| Open/resolved state | On decision node | Via `unknown` status | Not tracked structurally |
| New primitives | 2 kinds + 1 edge + 1 field | 1 kind + 1 edge + 1 field | 1 kind + 1 edge |
| Semantic cost | None | LOW (unknown carries dual role) | MEDIUM (pairs have no context) |
---
## Final Architectural Choice
### B — UNKNOWN + OPTION
**Chosen because it is the smallest model that satisfies all five decision-rule conditions.**
Minimum node kinds: `option` (1 new kind; reuses existing `unknown`)
Minimum relationships: `contained_in` (1 new relationship type)
Minimum fields: none required in initial design (baseline detection by label/consequence pattern is feasible later)
### Why B over A?
A adds a separate `decision` node kind, which is semantically cleaner for the "what's the question?" layer but costs one additional primitive. The incremental cleanliness of B is justified because:
- `unknown` naturally expresses "unresolved decision context" (the semantic overlap is natural, not forced)
- Question compatibility uses existing `selectedQuestion` infrastructure without extension
- Lifecycle mapping is identical to what the engine already tracks (open/resolved unknowns)
### Why B over C?
C fails on recoverability of decision context and open/resolved lifecycle. The cost savings (2 primitives vs 3) come at the expense of losing the question that makes two options meaningful as a pair. Two floating options are not a decision — they are just two things with a mutual-exclusion edge.
---
## Smallest Winning 59B.4 Graph
**Decision context:**
```text
[unknown: "Which option leaves us better off overall?"]
kind: unknown (existing)
status: unknown (existing)
id: n_active_unknown
label: "Relocate versus stay-put net value comparison"
```
**Options:**
```text
[option: Relocate]
kind: option (NEW)
status: unknown
contained_in → n_active_unknown (via new edge type)
[option: Stay put]
kind: option (NEW)
status: unknown
contained_in → n_active_unknown (via new edge type)
```
**Consequences (each on its own structural node):**
For relocate:
- `metric` — "Annual savings from relocation" — value=2000000 GBP/year — may_cause → option_relocate
- `observation` — "Two senior engineers leave" — may_cause → option_relocate
- `observation` — "Up to two months delivery delay" — may_cause → option_relocate
For stay put:
- `observation` — "Both engineers retained" — causes → option_stay_put
- `observation` — "Avoid delivery disruption" — causes → option_stay_put
- `metric` — "Continuing extra £2M/year" — value=2000000 GBP/year — may_cause → option_stay_put
**Relationships:**
- 6 consequence edges (3 per option, using existing `causes`/`may_cause` types)
- 2 membership edges: option_relocate.contained_in → unknown, option_stay_put.contained_in → unknown (new edge type)
**Graph-only recover decision:** YES — `unknown` node with `option` children IS the decision structure.
**Graph-only recover relocate:** YES — any node where `contained_in → n_active_unknown` and label contains "relocate."
**Graph-only recover stay-put:** YES — any node where `contained_in → n_active_unknown` and label contains "stay" or "current state."
**Graph-only attach consequences to correct option:** YES — each consequence edge's `fromNodeId` explicitly identifies the parent option.
**Decision can later resolve without semantic abuse:** YES — `unknown` transitions from `status=unknown` to `status=resolved`, and one option could get a distinguished marker (status=supported, or any existing convention). No abuse of unrelated statuses or node kinds required.
---
## Implementation Readiness
### A — READY FOR BOUNDED IMPLEMENTATION
The minimum new primitives and semantics are precise enough to implement:
**Schema changes (exact):**
```javascript
// In SituationKind enum:
option: "option" // a choice available within a decision context
// In SituationRelationship enum:
contained_in: "contained_in" // this option is contained within a decision/unknown context
// In situationNodeSchema — optional on option nodes only:
is_baseline: z.boolean().optional() // future extension, not required for v1
```
**Prompt additions (4 sentences):**
1. "When the answer presents competing alternatives for a decision, create one node of kind 'option' for each alternative."
2. "Connect each option to its decision context node using relationship 'contained_in'."
3. "If the answer references a do-nothing baseline, label the corresponding option clearly (e.g., 'Stay put', 'Current state'). Detection can be by label convention; no is_baseline field required in v1."
4. "Attach consequences of each option to that option node using existing causal edges (causes/may_cause/etc.)."
**No schema-level change to:** `SituationStatus`, existing edge types, graph topology rules, validation logic beyond accepting the two new enum values.
**If one more design question were needed**, it would be: "Should `option` nodes themselves track a lifecycle status (e.g., `status=chosen`) or should resolution flow entirely through the parent `unknown` node?" For v1 implementation, this is deferred — existing statuses on options are sufficient for initial use.
---
## Exact Smallest Implementation Boundary
Production code changed: NO
Prompt changed: NO
Validator changed: NO
Schema changed: NO
Tests changed: NO
Ollama calls: 0
Live API calls: 0
Vitest run: NO
Dev server disturbed: NO
---
## Documentation Updated
- `docs/experiment-60a2.md` (this file) — full evaluation of all three candidates, architectural choice, and rationale
- `docs/current-handoff.md` — appended 60A.2 entry to the latest section