diff --git a/.gitignore b/.gitignore index ae826045..5fc37320 100644 --- a/.gitignore +++ b/.gitignore @@ -117,15 +117,14 @@ scripts/Deployment/build_and_upload_release_agent_build.ps1 scripts/Deployment/V2/ # M2 — local raw session fixtures before anonymization (plan §4 M2) -tests/fixtures/enrollment-sessions-local/* -!tests/fixtures/enrollment-sessions-local/.gitkeep -!tests/fixtures/enrollment-sessions-local/README.md +tests/fixtures/enrollment-sessions-local/ .dotnet-home/ .vscode/ TODO.txt tools/orphan-analysis.py +tools/export-session-fixture.mjs tasks/ # Azure Table cleanup backups (dry-run exports + delete manifests) diff --git a/tests/fixtures/enrollment-sessions-local/.gitkeep b/tests/fixtures/enrollment-sessions-local/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/fixtures/enrollment-sessions-local/README.md b/tests/fixtures/enrollment-sessions-local/README.md deleted file mode 100644 index 8e9de63c..00000000 --- a/tests/fixtures/enrollment-sessions-local/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# Local Enrollment Session Fixtures — NOT committed - -This directory holds **raw** or **partially-anonymized** session exports used during the -M3 reducer bring-up. Files here are **excluded from git** — see root `.gitignore`. - -## Workflow (plan §4 M3) - -1. Export a session via `tools/export-session-fixture.mjs` (pipes through MCP - `get_session_events`). -2. Inspect the resulting `{sessionId}.jsonl` here. Adjust / anonymize as needed. -3. When a fixture is stable and anonymized, **move** it to - `tests/fixtures/enrollment-sessions/` and commit it with a short README entry - (see that directory's README for the commit policy). - -## Why this split - -Plan: "Lokal Rohdaten arbeiten, nicht committen bis stabil + anonymisiert." -Prod-session exports contain tenant identifiers, device IDs, and user names that must -never enter the repo history. The two-directory pattern forces an explicit "promote" -step with review. diff --git a/tools/export-session-fixture.mjs b/tools/export-session-fixture.mjs deleted file mode 100644 index e7cabd5e..00000000 --- a/tools/export-session-fixture.mjs +++ /dev/null @@ -1,219 +0,0 @@ -#!/usr/bin/env node -// ========================================================================= -// export-session-fixture.mjs -// -// Plan: plans/REFACTOR_AGENT_V2.md §4 M2. -// -// Exports a Prod session's event stream (via MCP `get_session_events`) into a -// DecisionSignal-shaped JSONL fixture. During the v11 transition period each -// signal is emitted with `Evidence.Kind = "Derived"` and -// `Evidence.Identifier = "legacy-export-v1"` because the Legacy event stream is -// not a true raw signal stream — the export is a derivation. Once a real V2 -// Agent runs and writes native SignalLog JSONL, fixtures can be captured with -// `Evidence.Kind = "Raw"` and this tool's role shrinks to anonymization only. -// -// M2 scope: scaffolding + schema validation. The MCP call is a placeholder — -// wire it to the real MCP client in M3 when the first fixture-driven reducer -// test needs Prod input. -// -// Usage: -// node tools/export-session-fixture.mjs --help -// node tools/export-session-fixture.mjs --dry-run --session-id= --out=tests/fixtures/enrollment-sessions-local/.jsonl -// node tools/export-session-fixture.mjs --validate= -// ========================================================================= - -import { argv, exit } from "node:process"; -import { writeFileSync, readFileSync } from "node:fs"; -import { dirname } from "node:path"; -import { mkdirSync, existsSync } from "node:fs"; - -const args = parseArgs(argv.slice(2)); - -if (args.help) { - printUsage(); - exit(0); -} - -if (args.validate) { - exit(validateJsonl(args.validate) ? 0 : 2); -} - -if (!args.sessionId) { - console.error("ERROR: --session-id= is required. See --help."); - exit(1); -} - -if (args.dryRun) { - // Produce a tiny synthetic fixture so downstream tooling can be wired up - // without a live MCP connection. Each signal has the full required shape - // per DecisionCore.Signals.DecisionSignal + Evidence. - const fixture = buildDryRunFixture(args.sessionId); - const outPath = args.out ?? `tests/fixtures/enrollment-sessions-local/${args.sessionId}.dryrun.jsonl`; - writeJsonl(outPath, fixture); - console.log(`Wrote ${fixture.length} dry-run signals to ${outPath}`); - console.log("Next: fill in MCP client wiring for real session export (M3 bring-up)."); - exit(0); -} - -// TODO(v2-m3): wire up real MCP `get_session_events` call here. -// The MCP client integration lives in src/McpServer/autopilot-monitor-mcp/; -// this script will import the client, authenticate with the admin credentials -// in the local environment, fetch the full event stream for the session, and -// convert each row via mapLegacyEventToDecisionSignal() below. -console.error( - "ERROR: Real MCP export is not wired up yet (plan §4 M3). " + - "Use --dry-run for the scaffolding path, or --validate to check an existing fixture." -); -exit(3); - -// --------------------------------------------------------------------- helpers - -function parseArgs(argv) { - const out = { dryRun: false, help: false }; - for (const arg of argv) { - if (arg === "--help" || arg === "-h") { out.help = true; continue; } - if (arg === "--dry-run") { out.dryRun = true; continue; } - const match = arg.match(/^--([\w-]+)(?:=(.*))?$/); - if (match) { - const [, key, value] = match; - const k = key.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); - out[k] = value ?? true; - } - } - return out; -} - -function printUsage() { - console.log( - ` -export-session-fixture.mjs — export a Prod session to DecisionSignal JSONL - -Arguments: - --session-id= Required for export. Legacy session ID as seen in the Sessions table. - --out= Output path. Default: tests/fixtures/enrollment-sessions-local/.jsonl. - --dry-run Produce a small synthetic fixture (no MCP call). For tooling scaffolding. - --validate= Validate an existing JSONL file's DecisionSignal shape + Pflicht-Evidence. No output. - --help | -h Print this text. - -Evidence policy (plan §2.2 / §4 M2): - During the Legacy->V2 transition, exported signals are emitted as Evidence.Kind="Derived" - with Identifier="legacy-export-v1" and DerivationInputs populated from the source event row. - This preserves replay determinism at the signal-log level while the raw event row gets - discarded after ingest. Raw-Evidence exports become available once V2 Agents run on the - session (they write native SignalLog JSONL directly). - -Output: - Each line = one JSON-serialized DecisionSignal record. Comment lines starting with '#' - and blank lines are allowed and ignored by the Replay Harness. -` - ); -} - -function buildDryRunFixture(sessionId) { - // Minimal three-signal synthetic session: start, one phase change, recover. - // Reducer handlers are not implemented yet (M3); the harness parses and counts. - const t0 = new Date("2026-04-20T10:00:00Z").toISOString(); - const t1 = new Date("2026-04-20T10:01:00Z").toISOString(); - const t2 = new Date("2026-04-20T10:02:00Z").toISOString(); - return [ - { - SessionSignalOrdinal: 0, - SessionTraceOrdinal: 0, - Kind: "SessionStarted", - KindSchemaVersion: 1, - OccurredAtUtc: t0, - SourceOrigin: "export-session-fixture/dry-run", - Evidence: { - Kind: "Synthetic", - Identifier: `session:${sessionId}:started`, - Summary: "Dry-run session-started marker", - }, - Payload: { sessionId }, - }, - { - SessionSignalOrdinal: 1, - SessionTraceOrdinal: 1, - Kind: "EspPhaseChanged", - KindSchemaVersion: 1, - OccurredAtUtc: t1, - SourceOrigin: "export-session-fixture/dry-run", - Evidence: { - Kind: "Derived", - Identifier: "legacy-export-v1", - Summary: "phase=AccountSetup (dry-run)", - DerivationInputs: { - legacyEventType: "esp_phase_changed", - legacyPhase: "AccountSetup", - }, - }, - Payload: { phase: "AccountSetup" }, - }, - { - SessionSignalOrdinal: 2, - SessionTraceOrdinal: 2, - Kind: "SessionRecovered", - KindSchemaVersion: 1, - OccurredAtUtc: t2, - SourceOrigin: "export-session-fixture/dry-run", - Evidence: { - Kind: "Synthetic", - Identifier: `session:${sessionId}:recovered`, - Summary: "Dry-run reboot/recover marker", - }, - Payload: {}, - }, - ]; -} - -function writeJsonl(path, records) { - const dir = dirname(path); - if (!existsSync(dir)) { - mkdirSync(dir, { recursive: true }); - } - const body = records.map((r) => JSON.stringify(r)).join("\n") + "\n"; - writeFileSync(path, body, { encoding: "utf-8" }); -} - -function validateJsonl(path) { - const text = readFileSync(path, { encoding: "utf-8" }); - let ok = true; - let n = 0; - for (const [i, rawLine] of text.split(/\r?\n/).entries()) { - const line = rawLine.trim(); - if (!line || line.startsWith("#")) continue; - let rec; - try { - rec = JSON.parse(line); - } catch (err) { - console.error(`Line ${i + 1}: JSON parse error — ${err.message}`); - ok = false; - continue; - } - n++; - const problems = validateSignalShape(rec); - if (problems.length) { - console.error(`Line ${i + 1}: ${problems.join("; ")}`); - ok = false; - } - } - console.log(`Validated ${n} signals from ${path}: ${ok ? "OK" : "FAIL"}`); - return ok; -} - -function validateSignalShape(rec) { - const errs = []; - for (const key of ["SessionSignalOrdinal", "SessionTraceOrdinal", "Kind", "KindSchemaVersion", "OccurredAtUtc", "SourceOrigin", "Evidence"]) { - if (!(key in rec)) errs.push(`missing ${key}`); - } - if (rec.KindSchemaVersion !== undefined && rec.KindSchemaVersion < 1) errs.push("KindSchemaVersion must be >= 1"); - if (rec.Evidence) { - const ev = rec.Evidence; - for (const key of ["Kind", "Identifier", "Summary"]) { - if (!(key in ev) || ev[key] === "") errs.push(`Evidence.${key} missing/empty`); - } - if (ev.Kind === "Derived" && (!ev.DerivationInputs || Object.keys(ev.DerivationInputs).length === 0)) { - errs.push("Evidence.Derived requires DerivationInputs"); - } - } - return errs; -}