Feature/product platform foundation v0.62 #1

Merged
robbond merged 683 commits from feature/product-platform-foundation-v0.62 into feature/emergent-unknowns-v0.5 2026-09-09 07:58:20 +01:00
2 changed files with 108 additions and 5 deletions
Showing only changes of commit ec167f2689 - Show all commits
+2 -2
View File
@@ -126,6 +126,6 @@ Answer before continuing:
*Created by Experiment 34. Updated by Experiments 3853, 54A. Branch: `feature/user-workspace-ux-v0.7`.*
### Return-to-Work Note (Experiment 53 → 54A → 54B)
### Return-to-Work Note (Experiment 53 → 54A → 54B → 54C)
Experiment 53 proved semantic separation was possible; 54A inspected whether the current SituationGraph preserves that distinction from source code alone (confirmed: it does not); 54B traced both flows to find where provenance is lost and whether it is recoverable. The key finding: supplied-versus-inferred provenance EXISTS upstream in evidenceRecordSchema (evidenceType enum) but is LOST at buildInitialGraph because the node schema has no provenance field and only IDs are added to evidenceIds as bare string references without type metadata. For updates, the answer survives through to applyValidatedProposal but carries no provenance onto nodes/edges. Provenance recovery from graph state alone is NOT possible — it requires access to upstream evidence arrays. What remains open: creating (not merely preserving) a node-level provenance field and propagating evidenceType through both buildInitialGraph and applyValidatedProposal. First file to inspect when resuming: `lib/reconstruction/schema.js` line 116-121 for the upstream evidenceType enum, then `lib/graph/builder.js` lines 61-70 (dead code), then `lib/graph/apply-proposal.js` lines 2719-2882 (update path with no provenance attachment).
Experiment 53 proved semantic separation was possible; 54A confirmed the SituationGraph cannot recover provenance from graph state alone; 54B showed supplied-versus-inferred distinction exists upstream in evidenceRecordSchema but is lost at buildInitialGraph because the node schema has no provenance field and evidenceIds carries only bare ID references. 54C inspected the normal answer-update boundary and found that while the raw user answer and validated model proposal are explicitly separate parameters at applyValidatedProposal's call site (orchestrator.js:683), they cannot be deterministically mapped to individual graph nodes within the proposal — the LLM independently generates additions from answer context with no provenance mapping. First function/file to inspect when resuming: `lib/graph/orchestrator.js` line 633 (buildGraphUpdatePrompt) to assess whether prompt structure carries any origin markers.
+106 -3
View File
@@ -5145,7 +5145,7 @@ This is actually useful: it clarifies that provenance IS recoverable from valida
The supplied-versus-inferred distinction is fully preserved in the upstream reconstruction/update pipeline output (the validated evidence arrays carry explicit evidenceType values for each record). However, this distinction is never encoded into the SituationGraph nodes during either initial build or update application. The evidenceIds field on nodes provides indirect access to provenance via ID references, but only if the original evidence data remains available downstream of the graph.
The core insight: provenance is not lost from the pipeline — it is preserved in the reconstruction output that feeds the builder. It IS lost when the builder converts that output into a SituationGraph because the node schema has no field to receive it. This means a fix would add a provenance field to the node schema and propagate evidenceType through buildInitialGraph and applyValidatedProposal.
The core insight: provenance is not lost from the pipeline — it is preserved in the reconstruction output that feeds the builder. It IS lost when the builder converts that output into a SituationGraph because the node schema has no field to receive it. This means any future implementation would need to add a provenance-bearing field to the node schema and propagate evidenceType through buildInitialGraph and applyValidatedProposal — though the specific implementation approach (field name, placement, propagation mechanism) remains undecided.
### Limitations
@@ -5209,10 +5209,113 @@ The user answer enters orchestrator.updateCase() as a parameter, gets embedded i
From graph alone: No — the SituationGraph schema has no provenance fields on nodes or edges, and evidenceIds only carries ID references without type metadata. From upstream data: Yes — the validated reconstruction output (returned by analyseScenario) includes a complete evidence array where each record has an explicit evidenceType enum field distinguishing supplied from inferred information.
#### Future Work Recommendation
#### Future Work Consideration (Beyond 54B Scope)
Create — not merely preserve. Adding a provenance field to the SituationNode schema and propagating evidenceType through both buildInitialGraph (lib/graph/builder.js) and applyValidatedProposal (lib/graph/apply-proposal.js) is required so that provenance is embedded in the graph state itself rather than remaining recoverable only from external upstream data. The existing evidenceIds reference system should be retained as a cross-reference layer, but the critical gap is the absence of a node-level provenance field.
Any future fix would need to embed provenance in graph state rather than keeping it only in external upstream data — potentially via a node-level provenance field and evidenceType propagation through buildInitialGraph and applyValidatedProposal, while retaining the existing evidenceIds reference system as a cross-reference layer. The specific implementation approach remains undecided. 54C examines whether update provenance is deterministically knowable at the application boundary before any fix is designed.
### Status
**Committed.** Report appended to design log. Branch: `feature/user-workspace-ux-v0.7`. Working tree clean before commit.
## Experiment 54C — Is Update Provenance Deterministically Knowable Before Graph Application? (2026-08-07)
### Objective
Determine whether production code can distinguish user-supplied material from model-proposed additions at the boundary before graph mutation occurs. This is source-trace only; no code changed, no tests run, no solution designed.
### Hypothesis
The active update pipeline holds the raw user answer and the validated model proposal as distinct inputs immediately before graph mutation. If so, origin may be deterministically knowable at that boundary even though the current graph does not store it.
### Files Actually Inspected
- `lib/graph/orchestrator.js` — lines 569768 (updateCase / updateCaseWithDependencies)
- `lib/graph/apply-proposal.js` — lines 27192912 (applyValidatedProposal)
- `lib/graph/update-proposal.js` — lines 1157 (parseGraphUpdateProposal, graphUpdateSchema field list)
- `lib/graph/schema.js` — lines 5598 (situationNodeSchema, situationEdgeSchema), lines 155163 (graphUpdateSchema), lines 174179 (updateCaseRequestSchema)
- `docs/design-evolution-log.md` — Experiment 54B section (lines 50805178)
- `docs/current-handoff.md` — lines 129131 (Return-to-Work Note)
Production files actually inspected: `lib/graph/orchestrator.js`, `lib/graph/apply-proposal.js`, `lib/graph/update-proposal.js`, `lib/graph/schema.js`.
Reconstruction files NOT inspected: `lib/reconstruction/schema.js`, `lib/analysis.js`, `lib/graph/builder.js` (per budget constraints).
### Update Flow Trace
| Stage | What contains the user answer? | What contains model proposals? | Are they separate? | Origin deterministically knowable? |
|-------|-------------------------------|--------------------------------|--------------------|-----------------------------------|
| HTTP request body → updateCaseWithDependencies (orchestrator.js:594) | `answer` from `parsedRequest.data.answer` (schema: z.string().min(1).max(5000)) | — | Yes | N/A — no proposal yet |
| LLM prompt construction (orchestrator.js:633-638) | `answer` embedded in prompt text as context | Model generates response with additions/proposals | Separated by mechanism: answer is context, model output is the new data | Only by knowing that `answer` was supplied and `rawResponse` came from the LLM. No metadata markers separate user text from model inference within the proposal itself. |
| parseGraphUpdateProposal (update-proposal.js:100-157) | Not present in parsed output — stripped during JSON parsing | `parsedProposal.proposal` (graphUpdateSchema: addedNodes, updatedNodes, addedEdges, etc.) | N/A — user answer is gone from the parsed proposal object | Merged_or_lost — the raw user answer is no longer part of the proposal object |
| Orchestrator call to applyValidatedProposal (orchestrator.js:683-688) | `answer` passed as separate function argument | `proposal: parsedProposal.proposal` passed as separate function argument | **explicitly_separate** — two distinct named parameters in one function call | **implicitly_distinguishable** — parameter names distinguish them, but the proposal object itself contains no metadata labeling which nodes/edges came from user input vs model inference |
| Inside applyValidatedProposal (apply-proposal.js:2719-2880) | `answer` used only in `deriveReasoningStateOverride` (line 2875-2878). No provenance metadata derived from it. | `proposal` (validated against graphUpdateSchema, reconciled via reconcileResolutionSemantics) — then snapshot at line 2869 | Still **explicitly_separate** within the function scope | The two inputs are separate variables, but the proposal object carries no origin labels on its nodes/edges |
| applyGraphUpdate calls (apply-proposal.js:2882, 2912) | `answer` not passed to applyGraphUpdate | proposalSnapshot applied to graphSnapshot. Nodes created without source metadata. | N/A — applyGraphUpdate receives only the merged snapshot | **merged_or_lost** — origin information is not transmitted to the mutation function |
| Resulting SituationGraph (after line 2912) | No record of which nodes/edges came from user | All new nodes/edges carry no provenance field | N/A | The graph stores only structural data; origin is unrecoverable from graph state alone |
### Answers to Required Questions
1. **Is the raw user answer still available immediately before proposal application?**
Yes. In orchestrator.js line 683-688, `answer` (from parsedRequest.data.answer) is passed as a named argument to applyValidatedProposal alongside `proposal`. Both exist as separate function arguments at the call site.
2. **Is the validated model proposal a separate object at that same point?**
Yes. `parsedProposal.proposal` is a distinct object from `answer`. It is the output of parseGraphUpdateProposal, validated against graphUpdateSchema, and passed as the `proposal` argument. The answer and proposal are different values in the JavaScript call stack.
3. **Does applyValidatedProposal receive both, or only the proposal/graph?**
Both. The function signature (line 2719) receives `{ situationGraph, proposal, previousQuestion, answer }`. All four are separate destructured parameters.
4. **Can deterministic code identify "user supplied" versus "model proposed" without asking the LLM?**
At the applyValidatedProposal call boundary: yes, by parameter identity. The `answer` argument contains user-supplied text; the `proposal` argument contains model-generated graph changes. These are distinguishable because they are different variables in the JavaScript runtime and come from different sources in orchestrator.js (user request vs LLM response).
However: within the proposal object itself, there is no metadata on individual nodes or edges indicating whether a specific node originated from user-supplied information or was model-inferred. The proposal schema (graphUpdateSchema) has no provenance field on addedNodes or addedEdges — situationNodeSchema contains only structural fields (id, label, description, kind, status, confidence, value, unit, evidenceIds, dependsOn, affects, parentId, childIds).
5. **At what exact function boundary does that distinction cease to be recoverable?**
The distinction is knowable at the `orchestrator.updateCaseWithDependencies` call site (line 683) because both `answer` and `proposal` are separate named arguments. It becomes **merged_or_lost** at two points:
a) Inside applyValidatedProposal: the `answer` parameter is passed only to `deriveReasoningStateOverride` and never used to annotate nodes/edges with source metadata. Origin information exists in scope but is not applied to the graph mutation path.
b) At `applyGraphUpdate` calls (lines 2882, 2912): only `graphSnapshot` and `proposalSnapshot` are passed. The `answer` parameter is discarded — it never reaches the mutation function that creates nodes/edges.
6. **Is the loss caused by which factor?**
**More than one boundary.** Specifically:
- Proposal schema (graphUpdateSchema/situationNodeSchema): no provenance field exists on addedNodes or addedEdges — this is the primary structural cause. If a provenance field existed, it could be populated.
- Application function signature (applyGraphUpdate at lines 2882/2912): `answer` is not passed to the mutation function that actually creates graph state. Even if nodes had provenance fields, the source information would need to be carried through to reach them.
- Graph schema: the resulting situationGraph has no provenance-aware structure (follow-on effect of proposal schema gap).
7. **Does the update path already contain enough information to assign provenance deterministically before graph storage?**
**No.** While user answer and model proposal are separate at the applyValidatedProposal call boundary, the answer text is a free-form string with no structural mapping to specific nodes in the proposal. The orchestrator does not know which parts of the LLM response were derived from user input versus independently inferred by the model. Even though both inputs exist as distinct parameters, there is no deterministic mechanism within the data flow to map user-supplied content to specific graph nodes/edges in the proposal.
### Experiment Conclusion
**Update origin is partially knowable but already merged before application.**
The raw user answer and validated model proposal are explicitly separate at the `applyValidatedProposal` function call boundary (orchestrator.js:683-688). This means deterministic code CAN identify "which argument is the user answer" and "which argument is the model proposal." However, this distinction does not translate to per-node provenance because:
1. The graphUpdateSchema / situationNodeSchema has no provenance field on nodes or edges.
2. The raw user answer text has no structural mapping to proposal node boundaries — the LLM consumes the answer as context and generates additions independently, so there is no way to deterministically say "this node contains user information" versus "this node contains model inference."
3. The `answer` parameter is not forwarded to applyGraphUpdate, the function that actually mutates graph state.
Provenance loss occurs at the intersection of proposal schema (no provenance field) and application logic (answer discarded before mutation).
### Limitations
- Source-trace audit only; no live model or parsing executed
- Did not examine prompt templates to determine if they carry answer-supplying markers
- Did not examine reconcileResolutionSemantics for any implicit origin tagging
- Did not examine applyGraphUpdate internals beyond the call signatures
### Status
**Pending Rob's review.** Source-trace complete. No production code changed. Working tree clean before commit.
### Production Unchanged
- `lib/graph/orchestrator.js`: 0 lines changed
- `lib/graph/apply-proposal.js`: 0 lines changed
- `lib/graph/update-proposal.js`: 0 lines changed
- `lib/graph/schema.js`: 0 lines changed
- No production files modified
- Working tree clean before commit
### Tests / Validation Run
No test run required; Experiment 54C is a source-trace audit.