diff --git a/CLAUDE.md b/CLAUDE.md index 3273fdfb..5c2a112c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -52,11 +52,14 @@ frontend (React/SSE) → backend (Fastify) → pi --mode rpc → tht/harness → There is no verbatim transcript store. A resumed Pi process rebuilds context from `tht session show ` + the on-disk artifacts. -- **The backend is a thin bridge with no database.** `ThtRunner` shells `tht` subcommands; - `PiProcessManager` runs one Pi child per session and bridges its RPC stream; +- **The backend is a thin bridge with no database of its own.** `ThtRunner` shells `tht` + subcommands; `PiProcessManager` runs one Pi child per session and bridges its RPC stream; `SessionBridge` maps Pi RPC events → client events (`ui_request`/`text_delta`/`info`); - `SseHub` fans them out over SSE to the browser. App settings live in a JSON file - (`backend/data/settings.json`), not a DB. + `SseHub` fans them out over SSE to the browser. Persistence belongs to the HARNESS, which + selects the session repository from the workspace config (`harness/tht/session/repository.py`): + filesystem by default, **PostgreSQL when `session_storage` is configured** (server/portable + deployment). Settings flow through harness preferences (`tht session preferences`) with + `backend/data/settings.json` only as the file fallback for injected runners/tests. - **Human-in-the-loop gate contract.** The model proposes; a human reviewer decides at gates via widgets (`reviewer_select` = single pick — a chosen option carrying a `decision` payload @@ -75,8 +78,9 @@ frontend (React/SSE) → backend (Fastify) → pi --mode rpc → tht/harness → - **Workspaces** (`harness/workspaces/*.yaml`) set the DB target and **absolute** `paths.sessions/artifacts/indexes` — for `psd` these point at a *separate, uncommitted* repo (`tht-workspace-psd/`). Secrets live ONLY in `harness/.env` (gitignored). -- **Settings are global** (`backend/data/settings.json`: workspace/provider/model/thinking); - the New-session form is question-only. +- **Settings are global** (workspace/provider/model/thinking, persisted via harness + preferences — `backend/data/settings.json` is only the fallback); the New-session form is + question-only. - **Resume**: a resumable session re-enters at its last incomplete phase. The backend refuses resume with 409 when `finalized` or `archived`, and `PiProcessManager.spawnFor` must send `/riprendi-sessione ` (resume mode) vs `/nuova-domanda` (new) — sending the wrong prompt diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index 3597c581..3f795eb5 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -346,8 +346,11 @@ All three layers' deps installed (`npm install` in each, `python -m venv + pip i repo `tht-workspace-psd/`, NOT committed here). - Secrets live ONLY in `harness/.env` (gitignored; `THT_*` — DB, DWH REST, vector, SSL CA…). See `harness/.env.example` for the variable list. -- App settings (global): `backend/data/settings.json` (gitignored) — `{ workspace, provider, - model, thinking }`. The "New session" form is question-only; these settings supply the rest. +- App settings (global): `{ workspace, provider, model, thinking }`, persisted via harness + preferences (`tht session preferences get/set` → the configured session repository — + filesystem or Postgres in server mode). `backend/data/settings.json` (gitignored) remains + only the file fallback for injected runners/tests. The "New session" form is question-only; + these settings supply the rest. ## Efficiency levers (NL→SQL workflow optimization, 2026-07-08) diff --git a/backend/src/routes/sessions.ts b/backend/src/routes/sessions.ts index d7d38169..6f228041 100644 --- a/backend/src/routes/sessions.ts +++ b/backend/src/routes/sessions.ts @@ -98,7 +98,11 @@ export function sessionRoutes( // old failure must not touch its manifest. const bound = boundRuntimes.get(id); if (bound !== undefined && bound !== rt) return; - await runner.failSession(id, workspace).catch(() => undefined); + // Best-effort by design (crash path), but a storage outage must be + // visible server-side: the manifest stays open and resume remains legal. + await runner.failSession(id, workspace).catch((error: unknown) => { + console.error(`[session:${id}] failSession persistence failed:`, error); + }); }).catch(() => undefined); } } diff --git a/docs/superpowers/plans/2026-07-20-full-audit-remediation-plan.md b/docs/superpowers/plans/2026-07-20-full-audit-remediation-plan.md new file mode 100644 index 00000000..06fb869a --- /dev/null +++ b/docs/superpowers/plans/2026-07-20-full-audit-remediation-plan.md @@ -0,0 +1,210 @@ +# Full-codebase audit — remediation plan (2026-07-20) + +Source: adversarial review (3 Codex reviewers — Skeptic/Architect/Minimalist — per the +adversarial-review skill) + independent verification of every finding + objective checks. +Baseline at commit `803b98d`: harness 812 pytest pass (l0/l2 deselected), backend 228, +frontend 309, both `tsc` clean, **ruff 34 errors** (all in `harness/tests`). + +Verdict: **REJECT** (per skill verdict logic: high-severity findings with multi-reviewer +consensus). The app is functional and green on tests, but ships 5 confirmed high-severity +defects. Nothing here is data-destroying in the happy path; the highs are silent-wrong-state +and workflow-integrity classes. + +Every finding below was re-verified in the current code by the lead (line numbers checked). +Two reviewer findings were **rejected** in lead judgment and are listed at the bottom. + +--- + +## FASE 1 — Pi-crash class closure (gate) — ~1h, no behavior change + +**1.1 [HIGH] Five gate tools can still crash Pi on an uncaught throw.** +`harness/.pi/extensions/tht-gate.js` — `reviewer_memory_promote` (execute at ~1343), +`rewrite_question` (~1431), `write_schema_linking` (~1484), `write_cte_sql` (~1511), +`write_final_sql` (~1527). Same class as the crash fixed in `87cb806` for the four +`reviewer_*` tools: `currentPhase()`/`phaseId()`/`tht()` run outside any try/catch; a +throw rejects the async execute → Node unhandled rejection → Pi dies mid-session. +FIX: wrap each execute body in the same top-level try/catch → `textResult` pattern. +VERIFY: `node -c`; grep-test asserting every `registerTool` execute has a top-level try. + +## FASE 2 — Workflow integrity (single-source the phase truth) — ~half day + +**2.1 [HIGH] WorkflowBar phase labels are fiction from F2 onward.** +`frontend/src/shell/WorkflowBar.tsx:5-29` hardcodes +`F2 "Schema linking" / F3 "Exploration" / F4 "SQL plan" / F5 "SQL generation" / F6 +"Validation" / F7 "Review"`, while `harness/workflow.yaml` defines +`F2 memoria / F3 riscrittura / F4 schema_linking / F5 sintesi / F6 cte / F7 sql_finale`. +Every live session shows the wrong phase name to the reviewer from F2 on. +FIX (minimal): correct the two static maps to the canonical sequence. +FIX (right): backend `GET /workflow/meta` shelling `tht phase meta --json`, frontend +fetches once per app load with the corrected static map as fallback. +VERIFY: unit test mapping `tht phase meta --json` ids/names ↔ rendered labels. + +**2.2 [HIGH] Gate `forceAdvance` contradicts the canonical SKILL.md.** +`harness/.pi/skills/tht-sessione/SKILL.md:30-32`: "`advance:true` auto-advances only F2 +(empty memory) and F6 (skipped/empty) — never a phase that recorded substantive +decisions." But since `6ee5bda`, `reviewer_decide` and `reviewer_schema_linking` with +`advance:true` **force-advance any phase** after persisting decisions +(`tht-gate.js` `forceAdvance`, used at ~895 and ~1002). The model reads one contract and +the gate implements another → nondeterministic workflow shape (phases may skip their +`reviewer_confirm kind:"phase"` summary gate depending on what the model passes). +The force-advance behavior was a deliberate product choice (kill the redundant approve +form); the defect is the contradiction, not the feature. +FIX: (a) restrict `forceAdvance` to the two designed auto-close gates +(`reviewer_schema_linking` in F4, `reviewer_memory_promote` in F8 via +`closeAfterPromotion`); `reviewer_decide` returns to `advanceIfReady` (exit-6 no-op); +(b) rewrite SKILL.md §"Phase map (advance cheat-sheet)" to document exactly which gates +auto-advance; (c) add the allowlist as data in `workflow.yaml` (e.g. `advance: +gate_closes`) so gate and skill read one source. +VERIFY: L1 test on the allowlist; live smoke on psd (F4 auto-advances, F5 does not). + +## FASE 3 — Transport correctness (SSE) — ~half day + +**3.1 [HIGH, 3/3 reviewer consensus] Stale `Last-Event-ID` from an older backend +generation is honored when ids collide.** +`backend/src/sse/sse-hub.ts:55-63`: the stale-cursor guard only catches +`cursor > lastId`. After a backend restart, ids restart at 1; a browser auto-reconnect +carrying cursor N from the old process silently suppresses the new generation's events +1..N (different content, same ids). Contract 11 in `brain/codebase/workflow-ui-contracts.md` +is only half-implemented. +FIX: make the event id a composite `":"` (generation = process-start +epoch, e.g. `Date.now()` at Hub construction). `Last-Event-ID` is opaque to EventSource, +so no frontend change: on subscribe, parse the cursor; generation mismatch → treat as 0. +VERIFY: regression test — two Hub instances, cursor from A replayed against B must +replay from the beginning. + +**3.2 [MEDIUM] Transport state never evicted for finished sessions.** +`sse-hub.ts:41-44` — `buffers`/`lastIds` grow per session; `forget()` is called only by +the delete route. Long-lived deployments accumulate state for every session ever touched. +FIX: call `hub.forget(id)` when a session reaches a terminal state (finalized+agent_end, +archived) — the transcript UI reads persisted documents, not the SSE buffer. +VERIFY: unit test — finalize → maps empty for that id. + +## FASE 4 — Process & route robustness (backend) — ~half day + +**4.1 [MEDIUM] `spawnFor` leaks a live child + registered runtime when `configure()` +rejects.** `backend/src/pi/pi-process-manager.ts:177-182`: `createFor` registers the +runtime; if `configure` (set_model/set_thinking RPC) rejects, `spawnFor` throws without +teardown → the map keeps an apparently-active runtime with a live Pi child; every later +start gets "session runtime already active". +FIX: try/catch in `spawnFor` → identity-checked teardown + rethrow. +VERIFY: unit test with a configure that rejects. + +**4.2 [MEDIUM] No timeout on any `tht` call except `dbPing`.** +`backend/src/tht/tht-runner.ts:67-101`: `run()` supports `timeoutMs` but only `dbPing` +passes one. A DWH/vector op that hangs (VPN drop mid-call: `sql preview`, `search pack`, +`ollama ensure`) wedges the HTTP request forever. +FIX: default timeout in `run()` (60s) + explicit per-call values (dbPing 10s, +sqlPreview/searchPack 120s). On timeout the existing code-124 path already SIGKILLs. +VERIFY: unit test with a sleeping fake bin. + +**4.3 [MEDIUM] Missing workspace YAML silently falls back to the default config.** +`tht-runner.ts:51-56`: `configArg("typo")` returns the default `-c` when +`workspaces/typo.yaml` doesn't exist → sessions/operations silently target the wrong +workspace (wrong DB, wrong sessions dir). +FIX: throw on a named-but-missing workspace; let routes surface 500 with the message. +VERIFY: unit test. + +**4.4 [MEDIUM] Resume returns `200 alreadyActive` before the finalized/archived 409.** +`backend/src/routes/sessions.ts:288-296`: the runtime fast-path short-circuits the +read-only contract. A finalized session with a lingering running runtime resumes as if +live. +FIX: evaluate the manifest 409 first; only then the alreadyActive fast-path. +VERIFY: existing route test extended (finalized manifest + fake active runtime → 409). + +**4.5 [LOW] `ollamaEnsure` treats exit-0 with unparseable stdout as ok.** +`tht-runner.ts:204-206`. FIX: on code 0 require parsed JSON (else ok:false with detail). + +**4.6 [LOW] `respond()` sends stale gate responses with the wrong Pi RPC id.** +`backend/src/bridge/session-bridge.ts:110-117`: a retried response for gate A while B is +pending goes out with B's `pendingPiId`; Pi's descriptor-id check re-loops it (transient, +self-healing). FIX: drop the response unless `pending && uiResponse.id === pending.id`. + +## FASE 5 — State integrity (harness + gate persistence) — ~1 day + +**5.1 [MEDIUM] `phase reopen` deletes artifacts before recording `phase_reopened`.** +`harness/tht/cli/phase_cmd.py:124-128`: crash between `teardown_snapshot()` and +`append_decisions()` → artifacts of later phases deleted but ledger still at the old +phase; resume enters a phase whose expected artifacts are gone. +FIX: append `phase_reopened` FIRST, then teardown, and make teardown idempotent + +re-runnable: on session load, if the folded phase is behind surviving later-phase +artifacts, re-run the teardown repair pass. (Ledger-first means the half-state is +"reopened with stale extra artifacts", which the repair pass cleans deterministically.) +VERIFY: unit test simulating the crash window (teardown raises after ledger append). + +**5.2 [MEDIUM] Schema-linking approval persists N decisions non-atomically.** +`tht-gate.js` ~967-1000: one `tht decision add` per table/column; a transient failure +mid-loop returns an error with the ledger half-written; a retry re-adds the first K +decisions (duplicate entries; projection is set-based so the artifact survives, but the +audit ledger lies). +FIX: batch the whole review into one CLI call (`tht decision add-batch --doc -` on the +model of the existing `add-join-set`), atomic single append. +VERIFY: L1 test; retry after injected failure produces no duplicates. + +**5.3 [HIGH] Anti-bypass hook does not cover bash writes to protected state.** +`tht-gate.js:141-158` + hook 550-585: `FORBIDDEN` blocks specific `tht` subcommands and +`GATE_CODE_FILES` blocks the `write`/`edit` TOOLS, but plain bash can still mutate +protected state: `echo '{"type":"phase_approved"...}' >> review_decisions.jsonl`, +`sed -i` on `session_manifest.yaml`, `cat > .pi/extensions/tht-gate.js`. +This is defense against a *confused* model (it has already edited its own gate once — +see memory), not a hostile one; perfect sandboxing is out of scope. +FIX: extend the bash branch of the tool_call hook with a protected-path pattern: block +any bash command whose text references `review_decisions.jsonl`, `session_manifest.yaml`, +`cte_plan.json` or `.pi/extensions/` in a mutating context (`>`, `>>`, `tee`, `sed -i`, +`mv`, `cp`, `rm`, `python … open(...,'w')`). Read-only mentions (cat/grep) stay allowed. +VERIFY: L1 tests on the pattern (block list + allow list). + +## FASE 6 — Docs & hygiene — ~1h + +**6.1 [MEDIUM] CLAUDE.md/PROJECT_STATE describe a storage model that no longer exists.** +CLAUDE.md says "backend … no database", "App settings live in a JSON file +(`backend/data/settings.json`)". Since the portable-deployment merge the truth is: +`harness/tht/session/repository.py:69-86` selects **PostgresSessionRepository** when +`session_storage` is configured (server mode) vs filesystem; backend settings go through +harness preferences (`backend/src/app.ts:61-80`), with the JSON file as fallback only. +FIX: update CLAUDE.md architecture bullets + PROJECT_STATE.md; add one line on when each +repository/settings path is active. + +**6.2 [LOW] ruff: 34 errors in `harness/tests` (19 E702, 13 F401, 1 F841, 1 F541).** +FIX: `ruff check . --fix` (14 auto), hand-fix the E702 semicolons. Add ruff to whatever +gate runs before commits (it exists in dev deps; it just isn't enforced). + +**6.3 [LOW] Replay server drifts from the real REST surface.** +`tools/replay/server.mjs`: `/me` is missing (SPA calls it on boot; replay serves the SPA +HTML → JSON parse noise); resume-shape drift was just fixed (`803b98d`) — audit the +remaining routes against `backend/src/routes/*.ts` and stub what the SPA actually calls. +VERIFY: replay boot with devtools console clean. + +**6.4 [LOW] `failSession` failures are silently discarded (deliberate).** +`backend/src/routes/sessions.ts:111`: keep the swallow (crash-path best effort) but add a +`console.error` so a storage outage is at least visible server-side. + +--- + +## Rejected reviewer findings (lead judgment) + +- *Minimalist:* "`harness/README.md` references `docs/testing.md` which is absent" — + **rejected**: the file exists (`harness/docs/testing.md`). +- *Minimalist:* "bridge `respond()` lets a stale response wedge gate B" — **downgraded** + to 4.6: Pi's own descriptor-id check re-loops the gate; the flaw is real but transient. +- *Skeptic:* "failSession swallow leaves contradictory state" [medium] — **downgraded** + to 6.4: the catch is deliberate crash-path tolerance; only observability is missing. + +## What went well (3/3 reviewers found no issues here) + +- Pi runtime identity checks, duplicate-start protection, per-session lifecycle + single-flight in the routes. +- `-c` per-subcommand placement and `--json` purity (checked end to end). +- Tool-event sanitization across the backend/client boundary (contract 5). +- Phase folding / ledger mutation core logic and its test coverage. + +## Suggested execution order + +Fase 1 (crash class, 1h) → 2.1+2.2 (workflow integrity) → 3.1 (SSE generation) → +4.1-4.4 → 5.1-5.3 → 3.2 + Fase 6. Fasi 1-4 are independent of each other and safe to +land as separate commits; 5.1 and 5.2 touch the ledger contract and deserve their own +review pass. + +## Environment note + +The machine's `codex` CLI was broken (configured default model requires a newer CLI); +upgraded via Homebrew 0.137.0 → 0.144.6 to run the reviewers. diff --git a/harness/tests/l0/test_db_connection.py b/harness/tests/l0/test_db_connection.py index f762427f..b8d58c61 100644 --- a/harness/tests/l0/test_db_connection.py +++ b/harness/tests/l0/test_db_connection.py @@ -7,7 +7,7 @@ is not assumed reliable' gains real teeth for the data layer. import pytest from sqlalchemy import create_engine, text -from tht.db.connection import can_create_in_schema, make_engine, ping, writable_tables +from tht.db.connection import can_create_in_schema, ping, writable_tables pytestmark = [pytest.mark.l0] diff --git a/harness/tests/l2/test_memory_save_one_real.py b/harness/tests/l2/test_memory_save_one_real.py index c7e2aa74..7861eff8 100644 --- a/harness/tests/l2/test_memory_save_one_real.py +++ b/harness/tests/l2/test_memory_save_one_real.py @@ -9,7 +9,6 @@ Run: pytest -m l2 tests/l2/test_memory_save_one_real.py -s (needs .env + VPN + """ from datetime import datetime from pathlib import Path -from unittest.mock import MagicMock import pytest diff --git a/harness/tests/l2/test_session_ablazione.py b/harness/tests/l2/test_session_ablazione.py index ef3172c0..d1c1b218 100644 --- a/harness/tests/l2/test_session_ablazione.py +++ b/harness/tests/l2/test_session_ablazione.py @@ -71,7 +71,7 @@ def test_ablazione_session_manual(l2_env, tmp_path): ["THT_DWH_API_KEY", "THT_VEC_API_KEY", "THT_VEC_WRITE_API_KEY"]) assert env_ok assert WORKSPACE.exists() - print(f"\n[L2 manual] launch: pi --mode rpc (cwd=harness/)") + print("\n[L2 manual] launch: pi --mode rpc (cwd=harness/)") print(f"[L2 manual] /nuova-domanda \"{QUESTION}\"") print("[L2 manual] confirm: sql_final.sql present + ledger has value_grounded/" "concept_formula_approved. Session dir:", tmp_path) diff --git a/harness/tests/l2/test_value_grounding_real.py b/harness/tests/l2/test_value_grounding_real.py index 0e63a844..6bf54676 100644 --- a/harness/tests/l2/test_value_grounding_real.py +++ b/harness/tests/l2/test_value_grounding_real.py @@ -25,7 +25,6 @@ def test_ablazione_returns_multiple_columns(l2_env): non-collapsing aggregation. Requires a built LSH index (tht lsh build).""" if not WORKSPACE.exists(): pytest.skip(f"per-customer workspace repo not found: {WORKSPACE} (clone tht-workspace-psd)") - from tht.config import LshConfig try: from tht.lshindex import load_index, query_index # ported with the lsh build path except ModuleNotFoundError: diff --git a/harness/tests/test_ctetest.py b/harness/tests/test_ctetest.py index 0074a207..890e7841 100644 --- a/harness/tests/test_ctetest.py +++ b/harness/tests/test_ctetest.py @@ -9,7 +9,6 @@ Pure logic, no DB. import json from datetime import date, datetime from decimal import Decimal -from pathlib import Path import pytest diff --git a/harness/tests/test_decisions_retract.py b/harness/tests/test_decisions_retract.py index 0d5bc696..cf002cae 100644 --- a/harness/tests/test_decisions_retract.py +++ b/harness/tests/test_decisions_retract.py @@ -8,7 +8,7 @@ def test_decision_retracted_in_audit_but_marked(tmp_path): d1 = append_decision( session, type="table_promoted", subject="phase:4", detail="t1", rationale="r" ) - d2 = append_decision( + append_decision( session, type="decision_retracted", subject="phase:4", diff --git a/harness/tests/test_execute_inject_limit.py b/harness/tests/test_execute_inject_limit.py index 00c905be..3449640d 100644 --- a/harness/tests/test_execute_inject_limit.py +++ b/harness/tests/test_execute_inject_limit.py @@ -6,7 +6,6 @@ These tests pin: LIMIT added when absent, respected when present, never added to non-query statements, and the truncation-detection contract (limit+1 rows). Pure logic, no DB. """ -import pytest import sqlglot from tht.execute import _inject_limit diff --git a/harness/tests/test_formula.py b/harness/tests/test_formula.py index ce2a2d12..e7e636b7 100644 --- a/harness/tests/test_formula.py +++ b/harness/tests/test_formula.py @@ -6,9 +6,7 @@ surfaces a candidate formula, the reviewer approves or rejects it (recorded via concept_formula_approved / concept_formula_rejected), and approved formulas are part of the schema-linking artifact. The store is frontmatter-YAML + SQL body. """ -from pathlib import Path -import pytest from tht.evidence.formula_store import ConceptFormula, retrieve_formula, save_formula diff --git a/harness/tests/test_freetext_interpretation.py b/harness/tests/test_freetext_interpretation.py index 0c49f2ea..329f9a97 100644 --- a/harness/tests/test_freetext_interpretation.py +++ b/harness/tests/test_freetext_interpretation.py @@ -7,7 +7,6 @@ enforced by the skill prose + gate, validated at L2; this test pins the RECORDIN contract the gate relies on: free text from "Altro" must round-trip into the decision rationale and survive persistence, never silently discarded. """ -from pathlib import Path from tht.decisions import append_decision, list_decisions diff --git a/harness/tests/test_mschema_eligibility.py b/harness/tests/test_mschema_eligibility.py index d71ec4cf..98b7d8a7 100644 --- a/harness/tests/test_mschema_eligibility.py +++ b/harness/tests/test_mschema_eligibility.py @@ -9,7 +9,6 @@ from datetime import datetime from tht.config import EligibilityConfig from tht.mschema.eligibility import classify_all, classify_column, effective_eligibility from tht.mschema.models import ( - Annotations, ColumnAnnotation, ColumnPhysical, PhysicalSchema, diff --git a/harness/tests/test_phase_effective.py b/harness/tests/test_phase_effective.py index ef65b529..eefcd985 100644 --- a/harness/tests/test_phase_effective.py +++ b/harness/tests/test_phase_effective.py @@ -17,7 +17,8 @@ def test_effective_decisions_excludes_stale_high_phase(tmp_path): Sequenza: approva 1, approva 2, reopen a 1, poi table_promoted:4. current_phase diventa 1 (il reopen). La table_promoted:4 e' di fase 4 > 1 -> stale. """ - s = tmp_path / "s"; s.mkdir() + s = tmp_path / "s" + s.mkdir() _d(s, "phase_approved", "phase:1") _d(s, "phase_approved", "phase:2") _d(s, "phase_reopened", "phase:1") @@ -30,7 +31,8 @@ def test_effective_decisions_excludes_stale_high_phase(tmp_path): def test_current_phase_after_reopen(tmp_path): """Il fold su audit-excluding-retracted gestisce correttamente il reopen e le ri-approvazioni.""" - s = tmp_path / "s"; s.mkdir() + s = tmp_path / "s" + s.mkdir() _d(s, "phase_approved", "phase:1") _d(s, "phase_approved", "phase:2") assert current_phase(s) == 3 # dopo 2 approvazioni -> fase 3 @@ -42,7 +44,8 @@ def test_current_phase_after_reopen(tmp_path): def test_retracted_decision_excluded_from_effective(tmp_path): """Una decisione ritirata (decision_retracted) e' esclusa dalla vista effective.""" - s = tmp_path / "s"; s.mkdir() + s = tmp_path / "s" + s.mkdir() d1 = _d(s, "table_promoted", "phase:4", detail="t1") _d(s, "decision_retracted", "phase:4", retracts=d1.seq) # senza approvazioni di fase, current_phase=1; table_promoted:4 e' gia' > 1. @@ -60,13 +63,15 @@ def test_retracted_decision_excluded_from_effective(tmp_path): def test_current_phase_starts_at_1(tmp_path): - s = tmp_path / "s"; s.mkdir() + s = tmp_path / "s" + s.mkdir() assert current_phase(s) == 1 def test_current_phase_clamps_at_max_plus_1(tmp_path): """Dopo tutte le approvazioni, current_phase = max_phase + 1.""" - s = tmp_path / "s"; s.mkdir() + s = tmp_path / "s" + s.mkdir() for n in range(1, 9): _d(s, "phase_approved", f"phase:{n}") assert current_phase(s) == 9 # max_phase(8) + 1 @@ -74,7 +79,8 @@ def test_current_phase_clamps_at_max_plus_1(tmp_path): def test_effective_keeps_low_phase_after_high_phase_rollback(tmp_path): """Rollback a F4 NON invalida le decisioni delle fasi 1-3 (che restano <= current_phase).""" - s = tmp_path / "s"; s.mkdir() + s = tmp_path / "s" + s.mkdir() _d(s, "concept_clarified", "phase:1", detail="x") _d(s, "question_rewritten", "phase:3", detail="q") _d(s, "table_promoted", "phase:4", detail="t") @@ -97,7 +103,8 @@ def test_effective_keeps_low_phase_after_high_phase_rollback(tmp_path): def test_effective_decisions_no_reopen_returns_all_non_retracted(tmp_path): """Senza reopen e senza retract, effective = tutte le decisioni (della fase corrente).""" - s = tmp_path / "s"; s.mkdir() + s = tmp_path / "s" + s.mkdir() _d(s, "concept_clarified", "phase:1", detail="x") _d(s, "phase_approved", "phase:1") assert current_phase(s) == 2 diff --git a/harness/tests/test_sql_preview_json.py b/harness/tests/test_sql_preview_json.py index a1d36997..5d0bba53 100644 --- a/harness/tests/test_sql_preview_json.py +++ b/harness/tests/test_sql_preview_json.py @@ -6,7 +6,6 @@ Pure-logic tests (no DB needed): """ import json -import pytest from tht.execute.limit import inject_limit_offset diff --git a/harness/tests/test_sqlcheck.py b/harness/tests/test_sqlcheck.py index e6e44832..696a9de1 100644 --- a/harness/tests/test_sqlcheck.py +++ b/harness/tests/test_sqlcheck.py @@ -12,7 +12,7 @@ from tht.mschema.models import ( PhysicalSchema, TablePhysical, ) -from tht.sqlcheck import CheckResult, validate_sql +from tht.sqlcheck import validate_sql def _schema() -> PhysicalSchema: diff --git a/harness/tests/test_taskdoc.py b/harness/tests/test_taskdoc.py index 62a62d7a..665524c5 100644 --- a/harness/tests/test_taskdoc.py +++ b/harness/tests/test_taskdoc.py @@ -1,10 +1,10 @@ -from pathlib import Path from tht.taskdoc import generate_task_doc def test_task_doc_includes_question_and_schema_scope(tmp_path): - s = tmp_path / "sess"; s.mkdir() + s = tmp_path / "sess" + s.mkdir() (s / "question.md").write_text("# Domanda\nQuanti pazienti?\n## Assunzioni\n- a") (s / "schema_linking.json").write_text( '{"question":"q","candidates":[{"kind":"table","name":"pazienti"}],"joins":[],"excluded":[],"open_questions":[]}' @@ -17,7 +17,8 @@ def test_task_doc_includes_question_and_schema_scope(tmp_path): def test_task_doc_never_embeds_full_physical_yaml(tmp_path): """physical.yaml e' fatale per un 35B/<200k (~190k token). Mai incorporarlo.""" - s = tmp_path / "sess"; s.mkdir() + s = tmp_path / "sess" + s.mkdir() (s / "question.md").write_text("q") # un physical.yaml enorme fuori dalla sessione (come in ChironeWp3: artifacts/mschema/) (s.parent / "physical.yaml").write_text("x: " + "y" * 800_000) @@ -27,7 +28,8 @@ def test_task_doc_never_embeds_full_physical_yaml(tmp_path): def test_task_doc_byte_budget_enforced_on_normal_input(tmp_path): - s = tmp_path / "sess"; s.mkdir() + s = tmp_path / "sess" + s.mkdir() (s / "question.md").write_text("q") doc = generate_task_doc(session_dir=s, phase=1) assert doc.byte_budget_ok is True @@ -35,7 +37,8 @@ def test_task_doc_byte_budget_enforced_on_normal_input(tmp_path): def test_task_doc_byte_budget_violation_flagged(tmp_path): """Se un artefatto di sessione e' enorme (input perverso), byte_budget_ok diventa False.""" - s = tmp_path / "sess"; s.mkdir() + s = tmp_path / "sess" + s.mkdir() (s / "question.md").write_text("q") (s / "schema_linking.json").write_text("x: " + "y" * 400_000) # ~400KB -> over budget doc = generate_task_doc(session_dir=s, phase=7) @@ -43,7 +46,8 @@ def test_task_doc_byte_budget_violation_flagged(tmp_path): def test_task_doc_carries_phase_header(tmp_path): - s = tmp_path / "sess"; s.mkdir() + s = tmp_path / "sess" + s.mkdir() (s / "question.md").write_text("q") doc = generate_task_doc(session_dir=s, phase=4) assert "fase 4" in doc.body.lower() or "fase 4" in doc.body @@ -54,7 +58,8 @@ def test_task_doc_excludes_stale_decisions_post_rollback(tmp_path): Dopo rollback a F4, una sql_approved:7 stale non appare nel brief delle decisioni.""" from tht.decisions import append_decision from tht.phase import current_phase - s = tmp_path / "sess"; s.mkdir() + s = tmp_path / "sess" + s.mkdir() (s / "question.md").write_text("q") # simula: lavoro fino a F7, poi rollback a F4 append_decision(s, type="phase_approved", subject="phase:1") diff --git a/harness/tests/test_teardown.py b/harness/tests/test_teardown.py index 8fe668c1..53a67c13 100644 --- a/harness/tests/test_teardown.py +++ b/harness/tests/test_teardown.py @@ -2,7 +2,8 @@ from tht.teardown import teardown_to_phase def test_teardown_to_phase_4_deletes_phase5plus_artifacts(tmp_path): - s = tmp_path / "sess"; s.mkdir() + s = tmp_path / "sess" + s.mkdir() (s / "schema_linking.json").write_text("{}") # F4 artifact (s / "cte_plan.json").write_text("[]") # F6 artifact (s / "ctes").mkdir() @@ -19,7 +20,8 @@ def test_teardown_to_phase_4_deletes_phase5plus_artifacts(tmp_path): def test_teardown_to_phase_1_deletes_everything(tmp_path): """Rollback a F1 cancella tutti gli artefatti (tutte le fasi > 1).""" - s = tmp_path / "sess"; s.mkdir() + s = tmp_path / "sess" + s.mkdir() (s / "question.md").write_text("q") # F3 artifact (s / "schema_linking.json").write_text("{}") # F4 artifact (s / "cte_plan.json").write_text("[]") # F6 artifact @@ -37,7 +39,8 @@ def test_teardown_to_phase_1_deletes_everything(tmp_path): def test_teardown_to_phase_7_preserves_phase6_artifacts(tmp_path): """Rollback a F7 preserva gli artefatti delle fasi <= 7.""" - s = tmp_path / "sess"; s.mkdir() + s = tmp_path / "sess" + s.mkdir() (s / "schema_linking.json").write_text("{}") # F4 artifact (s / "cte_plan.json").write_text("[]") # F6 artifact (s / "ctes").mkdir() @@ -52,14 +55,16 @@ def test_teardown_to_phase_7_preserves_phase6_artifacts(tmp_path): def test_teardown_to_phase_on_empty_session_is_noop(tmp_path): - s = tmp_path / "sess"; s.mkdir() + s = tmp_path / "sess" + s.mkdir() report = teardown_to_phase(s, target_phase=4) assert report.deleted_files == [] def test_teardown_to_phase_nonexistent_artifacts_no_error(tmp_path): """Se gli artefatti non esistono (sessione nuova), teardown non fallisce.""" - s = tmp_path / "sess"; s.mkdir() + s = tmp_path / "sess" + s.mkdir() report = teardown_to_phase(s, target_phase=1) assert report.deleted_files == [] @@ -67,7 +72,8 @@ def test_teardown_to_phase_nonexistent_artifacts_no_error(tmp_path): def test_teardown_deletes_orphaned_cte_files_not_in_current_plan(tmp_path): """Il bug degli orfani CTE: dopo un re-derive con piano diverso, i vecchi .sql non piu' nel piano restano su disco e bloccano finalize. teardown li pulisce.""" - s = tmp_path / "sess"; s.mkdir() + s = tmp_path / "sess" + s.mkdir() (s / "ctes").mkdir() (s / "ctes" / "old_cte.sql").write_text("SELECT 1") # orphan (s / "ctes" / "another.sql").write_text("SELECT 2") # orphan diff --git a/harness/tests/test_workspace.py b/harness/tests/test_workspace.py index e373b475..681d3d3c 100644 --- a/harness/tests/test_workspace.py +++ b/harness/tests/test_workspace.py @@ -1,4 +1,3 @@ -from pathlib import Path from tht.workspace import load_workspace, WorkspaceError diff --git a/tools/replay/server.mjs b/tools/replay/server.mjs index b7d44c56..f457757a 100644 --- a/tools/replay/server.mjs +++ b/tools/replay/server.mjs @@ -334,6 +334,17 @@ const server = createServer(async (req, res) => { } // --- REST stubs the SPA needs to boot ----------------------------------- + // The SPA queries /me at boot (principal for session scoping/admin UI). Falling + // through to the static handler served HTML that apiFetch tried to parse as JSON. + if (method === "GET" && path === "/me") { + return sendJson(res, 200, { + issuer: "replay", subject: "reviewer@replay", displayName: "Replay reviewer", isAdmin: true, + }); + } + // Boot-time best-effort warmup: a no-op in replay (no Pi runtime exists). + if (method === "POST" && path === "/runtime/prewarm") { + return sendNoContent(res); + } if (method === "GET" && path === "/health") { return sendJson(res, 200, { ok: true, replay: true, gates: GATES.length }); }