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 206 additions and 0 deletions
Showing only changes of commit 41ea2cb6b9 - Show all commits
+117
View File
@@ -795,6 +795,123 @@ Zero Open Questions ("You've now worked through all of the questions we surfaced
Report is the established Investigation culmination. No further product boundary is selected.
## v0.61 — Direct Initial-Decomposition Apparatus
**Status:** v0.61 Experiment 1 produced B ("USEFUL BUT MATERIAL UNCERTAINTY LOST"). This apparatus task was executed to establish direct initial-decomposition invocation for subsequent semantic experiments.
### Canonical production seam
The existing route `/api/cases/start` **is directly suitable** for curl/Postman/Claude experimentation:
```
┌───────────┐ POST /api/cases/start ┌──────────────┐
│ Scenario │ ───────────────────────────► │ startCase() │
│ text (req) │ { scenario, promptVersion? } │ │
│ │ │ analyseScenario │
│ │ │ buildInitialGraph │
│ │ │ selectUnknown │
└────────────┘ └──────────────┘
```
**No browser state required. No Investigation ID required by the route itself.** The route accepts `scenario` string directly and invokes the full production reasoning path (analyseScenario → buildInitialGraph → determineGraphBackedQuestion).
### Direct curl/Postman contract (Rob)
**Method:** POST
**URL:** `http://localhost:3000/api/cases/start`
**Content-Type:** `application/json`
**Request body schema:**
```json
{
"scenario": "<your scenario text here>",
"promptVersion": "v0.2"
}
```
- `scenario` (required): string, 110000 characters
- `promptVersion` (optional): `"v0.1"` or `"v0.2"` (defaults to `"v0.2"`)
**Response shape (success):**
```json
{
"success": true,
"summary": "<reconstruction summary>",
"situationGraph": { /* full graph with nodes/edges/reasoningState */ },
"selectedQuestion": { "id": "...", "question": "...", "reasoningPattern": "..." },
"diagnostics": { /* decompositionApplied, questionComplexityAssessment, etc. */ },
"assessment": { "phase": "...", "progress": "..." },
"modelName": "qwen-claude:latest",
"responseDurationMs": 3210,
"validationStatus": "valid",
"promptVersion": "v0.2"
}
```
**Response shape (failure):**
```json
{
"success": false,
"error": "<message>",
"statusCode": 400|500|502,
"validationErrors": [...],
"analysisErrors": [...],
"diagnostics": {...}
}
```
### Claude apparatus
**Needed:** YES — a thin CJS helper exists for repeated controlled experiments.
**Path:** `scripts/start-case-experiment-helper.cjs`
**Command:**
```bash
node scripts/start-case-experiment-helper.cjs "<scenario text>"
```
or
```bash
node scripts/start-case-experiment-helper.cjs --file scenario.json
```
**Input:** scenario string (positional arg or JSON file with `{ "scenario": "..." }`)
**Output:** structured JSON to stdout (success fields + endToEndElapsedMs)
**Retries:** NO — single call, no retry logic
**Canonical production logic duplicated:** NO — imports `startCase` from `lib/graph/orchestrator.js`, exercises identical code path
### Deterministic zero-live-call verification
| Check | Source | Result |
|---|---|---|
| Input reaches startCase seam | `tests/app/api/cases-start-route.test.js:15` | PASS (mocked analyseScenario verified) |
| Output passed through correctly | `tests/start-case-summary.test.js:81` | PASS (exact summary field round-trip) |
| Execution failure returns 400/5xx | `tests/app/api/cases-start-route.test.js:55,79` | PASS |
| Malformed JSON returns 500 | `tests/app/api/cases-start-route.test.js:100` | PASS |
| No retry occurs | source inspection (single await) | CONFIRMED |
| Schema validation present | `lib/graph/schema.js:202-205` | Zod enforced |
**Live model calls:** ZERO
**Build:** NOT required (scripts/test only, no production code changes)
**Playwright:** NOT used (decomposition-only experiments do not require browser instrumentation)
### Browser state investigation
| Question | Answer |
|---|---|
| Does the route mutate persistence? | NO — persistence is handled by ScenarioForm caller AFTER receiving result |
| Does it require an Investigation ID? | NO — route accepts scenario text directly; id comes from UI caller's state |
| Does it depend on browser localStorage? | NO — pure HTTP JSON exchange |
| Does it require any browser-only state? | NO |
### Decision: direct curl/Postman suitable = YES
**Why:** The `/api/cases/start` route is a thin layer (19 lines) over `startCase()` that validates input via Zod, calls the production function, and returns structured results. No browser state, no persistence side effects, no unrelated mutations. Identical behaviour to what ScenarioForm exercises in production.
### Playwright posture for v0.61 experiments
- **Playwright NOT default** for decomposition-only semantic experiments (apparatus reaches production reasoning path via direct import or HTTP)
- **Playwright REMAINS required** when the experiment concerns visible/browser behaviour, UI state transitions, or localStorage hydration
## Next restart point
> v0.60 is complete. Report is established as the culmination of an Investigation. No next product boundary is currently selected. Begin the next session by choosing the next unresolved user/product reasoning boundary from current product behaviour and founding principles, rather than continuing storage migration or assuming an old backlog item is next.
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env node
/**
* Minimal startCase (initial decomposition) experiment harness.
*
* Loads .env.local, invokes startCase() through the real production path,
* and prints a structured result for semantic evaluation.
*
* Usage:
* node scripts/start-case-experiment-helper.cjs "<scenario text>"
* node scripts/start-case-experiment-helper.cjs --file scenario.json
*
* Exit codes:
* 0 — success (structured result printed to stdout)
* 1 — execution failure or invalid input
*/
const dotenv = require("dotenv");
dotenv.config({ path: ".env.local" });
function assertRequiredEnv(name) {
const value = process.env[name];
if (!value) {
throw new Error(
`Live experiment requires ${name}. Set it in .env.local.\n` +
`Found: OLLAMA_BASE_URL=${process.env.OLLAMA_BASE_URL ?? "(missing)"}, ` +
`OLLAMA_MODEL=${process.env.OLLAMA_MODEL ?? "(missing)"}`
);
}
return value;
}
function readScenarioInput(argv) {
if (argv.includes("--file")) {
const idx = argv.indexOf("--file");
if (idx + 1 >= argv.length) throw new Error("--file requires a path argument");
const fs = require("fs");
const path = argv[idx + 1];
return JSON.parse(fs.readFileSync(path, "utf-8"));
}
// Use all positional args as the scenario text (join with space)
const startIdx = argv.findIndex(a => a !== "" && !a.startsWith("-") && a !== "--");
if (startIdx === -1 || startIdx >= argv.length) throw new Error("Usage: node scripts/start-case-experiment-helper.cjs \"<scenario>\" [--file path.json]");
return { scenario: argv.slice(startIdx).join(" ") };
}
async function runStartCaseExperiment(scenarioInput) {
const baseUrl = assertRequiredEnv("OLLAMA_BASE_URL");
const model = assertRequiredEnv("OLLAMA_MODEL");
if (baseUrl === "http://localhost:11434" || baseUrl === "http://127.0.0.1:11434") {
throw new Error(
`Live experiment harness refuses to use localhost fallback. ` +
`OLLAMA_BASE_URL=${baseUrl}. Configure a real host in .env.local.`
);
}
const { startCase } = await import("../../lib/graph/orchestrator.js");
const endToEndStartedAt = Date.now();
const result = await startCase(scenarioInput);
const endToEndElapsedMs = Date.now() - endToEndStartedAt;
return {
success: result.success,
summary: result.summary ?? null,
situationGraphNodeCount: result.situationGraph?.nodes?.length ?? 0,
situationGraphEdgeCount: result.situationGraph?.edges?.length ?? 0,
selectedQuestion: result.selectedQuestion?.question ?? null,
assessmentPhase: result.assessment?.phase ?? null,
assessmentProgress: result.assessment?.progress ?? null,
endToEndElapsedMs,
diagnostics: result.diagnostics ?? null,
error: result.success ? null : (result.error ?? "unknown"),
statusCode: result.statusCode ?? (result.success ? 200 : 500),
};
}
(async () => {
try {
const scenarioInput = readScenarioInput(process.argv);
const result = await runStartCaseExperiment(scenarioInput);
console.log(JSON.stringify(result, null, 2));
process.exit(result.success ? 0 : 1);
} catch (e) {
console.error(`APPARATUS FAILURE: ${e.message}`);
process.exit(1);
}
})();