docs(plan): workflow contract hardening — 7-task TDD plan
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,721 @@
|
||||
# Workflow Contract Hardening Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Fix the F6 CTE-approval dead-end, add a `tht`-mediated `schema_linking.json` writer/validator, and correct `SKILL.md` so it matches the real phase-advance contract.
|
||||
|
||||
**Architecture:** Three coordinated harness changes (Pi gate JS + `tht` Python CLI + SKILL doc). All logic lives behind the `tht` CLI (unit-tested with pytest); the gate is thin glue that shells the CLI. Build order 2→3→1 so the SKILL documents the final contract.
|
||||
|
||||
**Tech Stack:** Python 3.13 + Typer + pydantic + pytest (`typer.testing.CliRunner`); Node ESM Pi extension (`execFileSync`).
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Spec: `docs/superpowers/specs/2026-07-01-workflow-contract-hardening-design.md`.
|
||||
- Work on branch `feat/workflow-contract-hardening` (already checked out).
|
||||
- `tht` `-c/--config` is a PER-COMMAND option (append AFTER the subcommand). New CLI commands take `config: Path = CONFIG_OPT`.
|
||||
- `--json` / machine-read stdout must be pristine (only the intended value).
|
||||
- UI strings English; document CONTENT and CLI user messages stay Italian (matches existing `tht` CLI copy).
|
||||
- Run harness tests from `harness/`: `cd harness && .venv/bin/pytest -q` (line-length 100, `ruff check .`).
|
||||
- Do NOT change `_AUTO_ADVANCE_PHASES`, `workflow.yaml` semantics, or the F7 `sql_approved` path.
|
||||
- Commit messages end with the `Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>` trailer.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: `tht cte next` command (Part 2 — CLI primitive for the F6 fix)
|
||||
|
||||
**Files:**
|
||||
- Modify: `harness/tht/cli/cte_cmd.py` (add a `next` command near `list_cmd`, ~line 153)
|
||||
- Test: `harness/tests/test_cte_next.py` (create)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `tht.phase.next_cte(session_dir) -> str | None`; `cte_cmd._load_config_or_exit`, `load_session_or_exit`, `session_dir` (already imported in the module).
|
||||
- Produces: `tht cte next --session <id>` prints the first unapproved CTE name on stdout (nothing when none) — consumed by the gate in Task 2.
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create `harness/tests/test_cte_next.py`:
|
||||
|
||||
```python
|
||||
"""L1: `tht cte next` — the first plan CTE not yet approved (drives the F6 gate)."""
|
||||
import json
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from tht.cli.cte_cmd import cte_app
|
||||
from tht.config import DatabaseConfig
|
||||
from tht.decisions import append_decision
|
||||
from tht.session.store import create_session
|
||||
|
||||
|
||||
def _db():
|
||||
return DatabaseConfig(database="testdb", user="u", password="p", **{"schema": "public"}) # noqa: S106
|
||||
|
||||
|
||||
def _patch_cfg(monkeypatch, tmp_path):
|
||||
from tht.cli import cte_cmd
|
||||
|
||||
class FakePaths:
|
||||
sessions = tmp_path
|
||||
|
||||
class FakeCfg:
|
||||
paths = FakePaths()
|
||||
database = _db()
|
||||
|
||||
monkeypatch.setattr(cte_cmd, "_load_config_or_exit", lambda _: FakeCfg())
|
||||
|
||||
|
||||
def test_cte_next_first_unapproved(tmp_path, monkeypatch):
|
||||
m = create_session("q", _db(), tmp_path)
|
||||
(tmp_path / m.id / "cte_plan.json").write_text(json.dumps(["a", "b", "c"]))
|
||||
_patch_cfg(monkeypatch, tmp_path)
|
||||
res = CliRunner().invoke(cte_app, ["next", "--session", m.id])
|
||||
assert res.exit_code == 0, res.output
|
||||
assert res.output.strip() == "a"
|
||||
|
||||
|
||||
def test_cte_next_after_one_approval(tmp_path, monkeypatch):
|
||||
m = create_session("q", _db(), tmp_path)
|
||||
(tmp_path / m.id / "cte_plan.json").write_text(json.dumps(["a", "b", "c"]))
|
||||
append_decision(tmp_path / m.id, type="cte_approved", subject="a")
|
||||
_patch_cfg(monkeypatch, tmp_path)
|
||||
res = CliRunner().invoke(cte_app, ["next", "--session", m.id])
|
||||
assert res.exit_code == 0, res.output
|
||||
assert res.output.strip() == "b"
|
||||
|
||||
|
||||
def test_cte_next_empty_when_all_approved(tmp_path, monkeypatch):
|
||||
m = create_session("q", _db(), tmp_path)
|
||||
(tmp_path / m.id / "cte_plan.json").write_text(json.dumps(["a"]))
|
||||
append_decision(tmp_path / m.id, type="cte_approved", subject="a")
|
||||
_patch_cfg(monkeypatch, tmp_path)
|
||||
res = CliRunner().invoke(cte_app, ["next", "--session", m.id])
|
||||
assert res.exit_code == 0, res.output
|
||||
assert res.output.strip() == ""
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd harness && .venv/bin/pytest tests/test_cte_next.py -q`
|
||||
Expected: FAIL — `No such command 'next'` (exit_code != 0).
|
||||
|
||||
- [ ] **Step 3: Add the `next` command**
|
||||
|
||||
In `harness/tht/cli/cte_cmd.py`, immediately after the `@cte_app.command("list")` function (`list_cmd`), add:
|
||||
|
||||
```python
|
||||
@cte_app.command("next")
|
||||
def next_cmd(
|
||||
session: str = typer.Option(..., "--session"),
|
||||
config: Path = CONFIG_OPT,
|
||||
) -> None:
|
||||
"""Primo CTE del piano non ancora approvato (stdout pulito; vuoto se nessuno).
|
||||
|
||||
Sorgente unica dell'ordine di approvazione CTE (F6): il gate lo usa per
|
||||
registrare cte_approved sul NOME del CTE (non su 'phase:6')."""
|
||||
from tht.phase import next_cte
|
||||
|
||||
cfg = _load_config_or_exit(config)
|
||||
load_session_or_exit(cfg, session)
|
||||
nxt = next_cte(session_dir(cfg, session))
|
||||
if nxt:
|
||||
typer.echo(nxt)
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `cd harness && .venv/bin/pytest tests/test_cte_next.py -q`
|
||||
Expected: PASS (3 passed).
|
||||
|
||||
- [ ] **Step 5: Lint + commit**
|
||||
|
||||
```bash
|
||||
cd harness && .venv/bin/ruff check tht/cli/cte_cmd.py tests/test_cte_next.py
|
||||
git add harness/tht/cli/cte_cmd.py harness/tests/test_cte_next.py
|
||||
git commit -m "feat(cte): add 'tht cte next' — first unapproved plan CTE
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Gate approves each CTE by name (Part 2 — the bug fix)
|
||||
|
||||
**Files:**
|
||||
- Modify: `harness/.pi/extensions/tht-gate.js` (`reviewer_confirm` handler, the `kind === "cte_result" || kind === "sql"` branch, ~lines 623-641)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `tht cte next --session <id>` (Task 1); existing `tht(ctx, args)`, `relayIfThtFails`, `currentPhase`, `textResult`.
|
||||
- Produces: no new interface — corrects the persisted decision to `cte_approved:<cte name>` so `phase.approved_ctes`/`next_cte` recognize it and F6 can close.
|
||||
|
||||
- [ ] **Step 1: Replace the combined cte_result/sql branch**
|
||||
|
||||
In `harness/.pi/extensions/tht-gate.js`, find (inside the `reviewer_confirm` tool's `execute`, after the `kind === "cte_plan"` block):
|
||||
|
||||
```javascript
|
||||
if (kind === "cte_result" || kind === "sql") {
|
||||
const dt = kind === "sql" ? "sql_approved" : "cte_approved";
|
||||
const err = relayIfThtFails(
|
||||
ctx,
|
||||
[
|
||||
"decision",
|
||||
"add",
|
||||
"--session",
|
||||
session,
|
||||
"--type",
|
||||
dt,
|
||||
"--subject",
|
||||
`phase:${currentPhase(ctx, session)}`,
|
||||
],
|
||||
"",
|
||||
);
|
||||
if (err) return err;
|
||||
return textResult(`${kind} approvato (sessione ${session}).`);
|
||||
}
|
||||
```
|
||||
|
||||
Replace it with:
|
||||
|
||||
```javascript
|
||||
if (kind === "cte_result") {
|
||||
// The CTE under review is always next_cte (plan order is enforced by
|
||||
// `tht cte test`). Approve it BY NAME: `cte_approved` keys on the CTE
|
||||
// name (phase.approved_ctes / next_cte); a `phase:N` subject is rejected
|
||||
// by `decision add` (exit 5) and never satisfies F6's advance prereq.
|
||||
const cteName = tht(ctx, [
|
||||
"cte",
|
||||
"next",
|
||||
"--session",
|
||||
session,
|
||||
]).trim();
|
||||
if (!cteName) {
|
||||
return textResult(
|
||||
`Nessun CTE in attesa di approvazione (sessione ${session}).`,
|
||||
);
|
||||
}
|
||||
const err = relayIfThtFails(
|
||||
ctx,
|
||||
["decision", "add", "--session", session, "--type", "cte_approved", "--subject", cteName],
|
||||
"",
|
||||
);
|
||||
if (err) return err;
|
||||
return textResult(`CTE '${cteName}' approvato (sessione ${session}).`);
|
||||
}
|
||||
if (kind === "sql") {
|
||||
const err = relayIfThtFails(
|
||||
ctx,
|
||||
[
|
||||
"decision",
|
||||
"add",
|
||||
"--session",
|
||||
session,
|
||||
"--type",
|
||||
"sql_approved",
|
||||
"--subject",
|
||||
`phase:${currentPhase(ctx, session)}`,
|
||||
],
|
||||
"",
|
||||
);
|
||||
if (err) return err;
|
||||
return textResult(`SQL approvato (sessione ${session}).`);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify the gate JS still parses and its unit suite is green**
|
||||
|
||||
Run: `cd harness && node --check .pi/extensions/tht-gate.js && node --test .pi/extensions/gate/__tests__/`
|
||||
Expected: no syntax error; existing gate tests PASS (this branch is glue — its behavior is guaranteed by Task 1's `cte next` tests + the deferred live F6 check per the spec's Risks).
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add harness/.pi/extensions/tht-gate.js
|
||||
git commit -m "fix(gate): F6 approves each CTE by name, not 'phase:6'
|
||||
|
||||
reviewer_confirm kind:'cte_result' registered cte_approved --subject
|
||||
phase:6, which decision_cmd rejects (exit 5) and next_cte never
|
||||
recognizes — dead-ending F6. Derive the CTE name from 'tht cte next'
|
||||
(plan-order single source of truth) and approve by name. sql path
|
||||
(sql_approved:phase:N) unchanged.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: `store.set_schema_linking` (Part 3 — validate + write the artifact)
|
||||
|
||||
**Files:**
|
||||
- Modify: `harness/tht/session/store.py` (add `import json` if absent; add `set_schema_linking` near `set_question`, ~line 159)
|
||||
- Test: `harness/tests/test_set_schema_linking.py` (create)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `SchemaLinking` model (`tht.session.models`); `load_session`, `touch_manifest` (already in `store.py`).
|
||||
- Produces: `set_schema_linking(session_id: str, data: dict, sessions_root: Path) -> Path` — validates against `SchemaLinking` (raises `pydantic.ValidationError`), writes `schema_linking.json` deterministically, returns the path. Consumed by Task 4.
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create `harness/tests/test_set_schema_linking.py`:
|
||||
|
||||
```python
|
||||
"""L1: store.set_schema_linking — validate against SchemaLinking, then write."""
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from tht.config import DatabaseConfig
|
||||
from tht.session.models import SchemaLinking
|
||||
from tht.session.store import create_session, set_schema_linking
|
||||
|
||||
|
||||
def _db():
|
||||
return DatabaseConfig(database="testdb", user="u", password="p", **{"schema": "public"}) # noqa: S106
|
||||
|
||||
|
||||
def test_writes_and_revalidates(tmp_path):
|
||||
m = create_session("q", _db(), tmp_path)
|
||||
data = {
|
||||
"question": "q riscritta",
|
||||
"candidates": [{"kind": "table", "name": "fact_x", "decision": "promoted"}],
|
||||
"joins": [{"from": "a.k", "to": "b.k"}],
|
||||
}
|
||||
path = set_schema_linking(m.id, data, tmp_path)
|
||||
assert path.exists()
|
||||
reloaded = json.loads(path.read_text())
|
||||
assert reloaded["candidates"][0]["name"] == "fact_x"
|
||||
assert reloaded["joins"][0]["from"] == "a.k" # 'from' alias round-trips
|
||||
SchemaLinking.model_validate(reloaded) # re-validates clean
|
||||
|
||||
|
||||
def test_rejects_invalid_and_writes_nothing(tmp_path):
|
||||
m = create_session("q", _db(), tmp_path)
|
||||
with pytest.raises(ValidationError):
|
||||
set_schema_linking(m.id, {"question": "q", "bogus": 1}, tmp_path) # extra=forbid
|
||||
assert not (tmp_path / m.id / "schema_linking.json").exists()
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd harness && .venv/bin/pytest tests/test_set_schema_linking.py -q`
|
||||
Expected: FAIL — `ImportError: cannot import name 'set_schema_linking'`.
|
||||
|
||||
- [ ] **Step 3: Implement `set_schema_linking`**
|
||||
|
||||
In `harness/tht/session/store.py`: ensure `import json` is present near the top imports (add it if missing). Then add, right after the `set_question` function:
|
||||
|
||||
```python
|
||||
def set_schema_linking(
|
||||
session_id: str,
|
||||
data: dict,
|
||||
sessions_root: Path,
|
||||
) -> Path:
|
||||
"""Valida e scrive schema_linking.json (Fase 4) in modo deterministico.
|
||||
|
||||
Valida `data` contro il modello SchemaLinking (ValidationError se invalido) PRIMA
|
||||
di scrivere, così un artefatto malformato non tocca mai il disco. Ritorna il path.
|
||||
"""
|
||||
from tht.session.models import SchemaLinking
|
||||
|
||||
load_session(session_id, sessions_root)
|
||||
model = SchemaLinking.model_validate(data)
|
||||
path = sessions_root / session_id / "schema_linking.json"
|
||||
path.write_text(
|
||||
json.dumps(model.model_dump(by_alias=True), indent=2, ensure_ascii=False)
|
||||
)
|
||||
touch_manifest(session_id, sessions_root)
|
||||
return path
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `cd harness && .venv/bin/pytest tests/test_set_schema_linking.py -q`
|
||||
Expected: PASS (2 passed).
|
||||
|
||||
- [ ] **Step 5: Lint + commit**
|
||||
|
||||
```bash
|
||||
cd harness && .venv/bin/ruff check tht/session/store.py tests/test_set_schema_linking.py
|
||||
git add harness/tht/session/store.py harness/tests/test_set_schema_linking.py
|
||||
git commit -m "feat(store): set_schema_linking validates then writes the F4 artifact
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: `tht session set-schema-linking` CLI (Part 3 — CLI surface)
|
||||
|
||||
**Files:**
|
||||
- Modify: `harness/tht/cli/session_cmd.py` (add command after `set_question_cmd`, ~line 111)
|
||||
- Test: `harness/tests/test_set_schema_linking_cli.py` (create)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `store.set_schema_linking` (Task 3); `_load_config_or_exit`, `load_session_or_exit`, `CONFIG_OPT` (already in module).
|
||||
- Produces: `tht session set-schema-linking <id> --file <path|->` — reads JSON (file or stdin `-`), validates+writes, exit 5 on bad JSON or `ValidationError`. Consumed by the gate in Task 5.
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create `harness/tests/test_set_schema_linking_cli.py`:
|
||||
|
||||
```python
|
||||
"""L1: `tht session set-schema-linking` — stdin/file JSON → validate → write."""
|
||||
import json
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from tht.cli.session_cmd import session_app
|
||||
from tht.config import DatabaseConfig
|
||||
from tht.session.store import create_session
|
||||
|
||||
|
||||
def _db():
|
||||
return DatabaseConfig(database="testdb", user="u", password="p", **{"schema": "public"}) # noqa: S106
|
||||
|
||||
|
||||
def _patch_cfg(monkeypatch, tmp_path):
|
||||
from tht.cli import session_cmd
|
||||
|
||||
class FakePaths:
|
||||
sessions = tmp_path
|
||||
|
||||
class FakeCfg:
|
||||
paths = FakePaths()
|
||||
database = _db()
|
||||
|
||||
monkeypatch.setattr(session_cmd, "_load_config_or_exit", lambda _: FakeCfg())
|
||||
|
||||
|
||||
_VALID = json.dumps({
|
||||
"question": "q",
|
||||
"candidates": [{"kind": "table", "name": "fact_x", "decision": "promoted"}],
|
||||
})
|
||||
|
||||
|
||||
def test_cli_stdin_writes(tmp_path, monkeypatch):
|
||||
m = create_session("q", _db(), tmp_path)
|
||||
_patch_cfg(monkeypatch, tmp_path)
|
||||
res = CliRunner().invoke(session_app, ["set-schema-linking", m.id, "--file", "-"], input=_VALID)
|
||||
assert res.exit_code == 0, res.output
|
||||
assert (tmp_path / m.id / "schema_linking.json").exists()
|
||||
|
||||
|
||||
def test_cli_file_writes(tmp_path, monkeypatch):
|
||||
m = create_session("q", _db(), tmp_path)
|
||||
p = tmp_path / "sl.json"
|
||||
p.write_text(_VALID)
|
||||
_patch_cfg(monkeypatch, tmp_path)
|
||||
res = CliRunner().invoke(session_app, ["set-schema-linking", m.id, "--file", str(p)])
|
||||
assert res.exit_code == 0, res.output
|
||||
assert (tmp_path / m.id / "schema_linking.json").exists()
|
||||
|
||||
|
||||
def test_cli_invalid_json_exit5(tmp_path, monkeypatch):
|
||||
m = create_session("q", _db(), tmp_path)
|
||||
_patch_cfg(monkeypatch, tmp_path)
|
||||
res = CliRunner().invoke(session_app, ["set-schema-linking", m.id, "--file", "-"], input="{not json")
|
||||
assert res.exit_code == 5
|
||||
assert not (tmp_path / m.id / "schema_linking.json").exists()
|
||||
|
||||
|
||||
def test_cli_invalid_model_exit5(tmp_path, monkeypatch):
|
||||
m = create_session("q", _db(), tmp_path)
|
||||
_patch_cfg(monkeypatch, tmp_path)
|
||||
res = CliRunner().invoke(
|
||||
session_app, ["set-schema-linking", m.id, "--file", "-"], input='{"question":"q","bogus":1}'
|
||||
)
|
||||
assert res.exit_code == 5
|
||||
assert not (tmp_path / m.id / "schema_linking.json").exists()
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd harness && .venv/bin/pytest tests/test_set_schema_linking_cli.py -q`
|
||||
Expected: FAIL — `No such command 'set-schema-linking'`.
|
||||
|
||||
- [ ] **Step 3: Add the command**
|
||||
|
||||
In `harness/tht/cli/session_cmd.py`, right after `set_question_cmd` (before the `@session_app.command("show")` block), add:
|
||||
|
||||
```python
|
||||
@session_app.command("set-schema-linking")
|
||||
def set_schema_linking_cmd(
|
||||
session_id: str = typer.Argument(...),
|
||||
file: str = typer.Option(
|
||||
..., "--file", "-f",
|
||||
help="Path al JSON dello schema-linking, oppure '-' per leggere da stdin."),
|
||||
config: Path = CONFIG_OPT,
|
||||
) -> None:
|
||||
"""Valida (modello SchemaLinking) e scrive schema_linking.json deterministicamente."""
|
||||
import sys
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
from tht.session.store import set_schema_linking
|
||||
|
||||
cfg = _load_config_or_exit(config)
|
||||
load_session_or_exit(cfg, session_id)
|
||||
raw = sys.stdin.read() if file == "-" else Path(file).read_text()
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except json.JSONDecodeError as e:
|
||||
typer.secho(f"ERRORE: JSON non valido: {e}", fg=typer.colors.RED, err=True)
|
||||
raise typer.Exit(code=5)
|
||||
try:
|
||||
path = set_schema_linking(session_id, data, cfg.paths.sessions)
|
||||
except ValidationError as e:
|
||||
typer.secho(f"ERRORE: schema_linking non valido:\n{e}", fg=typer.colors.RED, err=True)
|
||||
raise typer.Exit(code=5)
|
||||
typer.secho(f"OK: schema_linking.json aggiornato ({path}).", fg=typer.colors.GREEN)
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `cd harness && .venv/bin/pytest tests/test_set_schema_linking_cli.py -q`
|
||||
Expected: PASS (4 passed).
|
||||
|
||||
- [ ] **Step 5: Lint + commit**
|
||||
|
||||
```bash
|
||||
cd harness && .venv/bin/ruff check tht/cli/session_cmd.py tests/test_set_schema_linking_cli.py
|
||||
git add harness/tht/cli/session_cmd.py harness/tests/test_set_schema_linking_cli.py
|
||||
git commit -m "feat(cli): 'tht session set-schema-linking' (file/stdin, validated)
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Gate `write_schema_linking` tool + stdin support in `tht()` (Part 3 — gate glue)
|
||||
|
||||
**Files:**
|
||||
- Modify: `harness/.pi/extensions/tht-gate.js` (extend `tht()` ~line 147; add a `write_schema_linking` tool next to `rewrite_question`)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `tht session set-schema-linking <id> --file -` (Task 4); existing `execFileSync`, `loadEnvFromDotenv`, `textResult`, `lockActive`.
|
||||
- Produces: gate tool `write_schema_linking({session, schema_linking})` for the model to call in F4.
|
||||
|
||||
- [ ] **Step 1: Add optional stdin `input` to the `tht()` helper**
|
||||
|
||||
In `harness/.pi/extensions/tht-gate.js`, find:
|
||||
|
||||
```javascript
|
||||
function tht(ctx, args) {
|
||||
loadEnvFromDotenv(ctx);
|
||||
return execFileSync("tht", args, { cwd: ctx.cwd, encoding: "utf8" });
|
||||
}
|
||||
```
|
||||
|
||||
Replace with (optional third arg; existing 2-arg callers pass `input: undefined` = no stdin):
|
||||
|
||||
```javascript
|
||||
function tht(ctx, args, input) {
|
||||
loadEnvFromDotenv(ctx);
|
||||
return execFileSync("tht", args, { cwd: ctx.cwd, encoding: "utf8", input });
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Register the `write_schema_linking` tool**
|
||||
|
||||
In `harness/.pi/extensions/tht-gate.js`, immediately after the `rewrite_question` tool registration block (`pi.registerTool({ name: "rewrite_question", ... });`), add:
|
||||
|
||||
```javascript
|
||||
pi.registerTool({
|
||||
name: "write_schema_linking",
|
||||
label: "Scrittura schema_linking.json (validata)",
|
||||
description:
|
||||
"Scrive deterministicamente schema_linking.json validandolo contro il modello " +
|
||||
"SchemaLinking via tht session set-schema-linking (evita edit a mano e la " +
|
||||
"validazione manuale). schema_linking e' l'oggetto JSON completo: {question, " +
|
||||
"candidates:[{kind:'table'|'column', name, evidence?, decision?}], joins:[{from, " +
|
||||
"to, source?}], excluded:[{kind, name}], open_questions:[], concept_formulas:[]}.",
|
||||
parameters: Type.Object({
|
||||
session: Type.String(),
|
||||
schema_linking: Type.Any(),
|
||||
}),
|
||||
async execute(_id, params, _signal, _onUpdate, ctx) {
|
||||
lockActive = true;
|
||||
const { session, schema_linking } = params;
|
||||
try {
|
||||
tht(
|
||||
ctx,
|
||||
["session", "set-schema-linking", session, "--file", "-"],
|
||||
JSON.stringify(schema_linking),
|
||||
);
|
||||
return textResult(
|
||||
`schema_linking.json scritto e validato per la sessione ${session}.`,
|
||||
);
|
||||
} catch (e) {
|
||||
const cliMsg = (e.stderr || e.message || String(e)).toString().trim();
|
||||
return textResult(`${cliMsg} Correggi schema_linking e riprova.`);
|
||||
}
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify the gate JS parses and its unit suite is green**
|
||||
|
||||
Run: `cd harness && node --check .pi/extensions/tht-gate.js && node --test .pi/extensions/gate/__tests__/`
|
||||
Expected: no syntax error; existing gate tests PASS (the tool is glue over the Task 4 CLI, which is unit-tested; live F4 check deferred per the spec).
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add harness/.pi/extensions/tht-gate.js
|
||||
git commit -m "feat(gate): write_schema_linking tool (validated F4 artifact via CLI)
|
||||
|
||||
tht() gains an optional stdin arg; the tool pipes the schema-linking
|
||||
object to 'tht session set-schema-linking --file -', which validates
|
||||
against SchemaLinking and returns the exact error on failure — so the
|
||||
model stops hand-writing the artifact and validating with ad-hoc python.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: `SKILL.md` — correct the advance contract + cheat-sheet (Part 1)
|
||||
|
||||
**Files:**
|
||||
- Modify: `harness/.pi/skills/tht-sessione/SKILL.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: the behavior established in Tasks 1-5 (F6 approves by name; `write_schema_linking` exists).
|
||||
- Produces: documentation only — no code depends on it.
|
||||
|
||||
- [ ] **Step 1: Add the phase cheat-sheet**
|
||||
|
||||
After the "Language contract" paragraph (the block ending `...ask the reviewer.`, ~line 24) and before `## Disciplines`, insert:
|
||||
|
||||
```markdown
|
||||
## Phase map (advance cheat-sheet)
|
||||
|
||||
A phase advances ONLY when a `phase_approved:phase:N` decision is recorded for the current
|
||||
phase — written by `reviewer_confirm kind:"phase"` (or `kind:"sql"` for F7). A `reviewer_decide`/
|
||||
`reviewer_select` choice records its OWN decision but does NOT advance the phase. `advance:true`
|
||||
auto-advances only F2 (empty memory) and F6 (skipped/empty) — never a phase that recorded
|
||||
substantive decisions.
|
||||
|
||||
| Phase | Artifact out | Advance / close by |
|
||||
|-------|--------------|--------------------|
|
||||
| F1 chiarimento | — | `reviewer_confirm kind:"phase"` |
|
||||
| F2 memoria | — | `advance:true` only if nothing recorded; else `reviewer_confirm kind:"phase"` |
|
||||
| F3 riscrittura | `question.md` | `reviewer_confirm kind:"phase"` (after `rewrite_question`) |
|
||||
| F4 schema_linking | `schema_linking.json` | `reviewer_confirm kind:"phase"` (after `write_schema_linking`) |
|
||||
| F5 sintesi | — | `reviewer_confirm kind:"phase"` (after `tht session check`) |
|
||||
| F6 cte | `cte_plan.json`, `ctes/`, `cte_tests.json` | approve each CTE `kind:"cte_result"`, then `reviewer_confirm kind:"phase"` |
|
||||
| F7 sql_finale | `sql_final.sql` | `reviewer_confirm kind:"sql"` |
|
||||
| F8 datamart | — | `reviewer_confirm kind:"phase"` |
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Fix Discipline 2**
|
||||
|
||||
Replace the Discipline 2 block (the `2. **The choice is the confirmation.** ... never as a redundant echo of a recorded choice.` paragraph):
|
||||
|
||||
```markdown
|
||||
2. **The choice records; the phase gate advances.** A `reviewer_decide`, or a
|
||||
`reviewer_select` whose chosen option carries a `decision`, PERSISTS that decision — it
|
||||
does NOT by itself advance the phase. To move to the next phase you MUST issue
|
||||
`reviewer_confirm kind:"phase"` (F7 uses `kind:"sql"`), the deliberate "this phase is
|
||||
done" gate. The `advance:true` flag on `reviewer_decide` is a shortcut that auto-advances
|
||||
ONLY F2 when the memory phase recorded nothing and F6 when it is skipped/empty; everywhere
|
||||
else it is a silent no-op, so never rely on it to advance. Do NOT add a `reviewer_confirm`
|
||||
that merely echoes a decision already recorded by a choice — the phase gate is a separate,
|
||||
deliberate step, not an echo of a decision.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Fix Phase 1 step 4 (remove the mis-placed `rewrite_question`)**
|
||||
|
||||
Replace Phase 1 step 4 (`4. After the phase advance, update the question with the gate's rewrite_question ... never edit question.md by hand).`):
|
||||
|
||||
```markdown
|
||||
4. Closing Phase 1 advances to Phase 2 (Memories). The question is rewritten later, in
|
||||
Phase 3 — do NOT call `rewrite_question` here.
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Fix Phase 3 steps 2-3 (F3 does NOT auto-advance)**
|
||||
|
||||
Replace Phase 3 steps 2 and 3 (`2. Present in **a single** reviewer_decide(advance:true, allow_other:true). ...` through `... Never edit/write question.md manually.`):
|
||||
|
||||
```markdown
|
||||
2. Present in **a single** `reviewer_decide(advance:false, allow_other:true)`. The
|
||||
"Confirm rewriting" option is `recommended:true` with
|
||||
`{type:"question_rewritten", subject:"domanda", detail:"<full rewritten question>"}`.
|
||||
3. **Order matters:** (a) the `reviewer_decide` records `question_rewritten` → (b) call the
|
||||
gate's `rewrite_question` tool, which runs `tht session set-question` to write
|
||||
`question.md` (regenerates question + an "## Assunzioni" section; never edit it by hand)
|
||||
→ (c) close the phase with `reviewer_confirm kind:"phase"`. F3 does NOT auto-advance:
|
||||
the `question_rewritten` decision alone does not move the phase.
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Fix Phase 4 (advance:false + use `write_schema_linking`)**
|
||||
|
||||
In Phase 4 step 2, change `reviewer_decide(advance:true)` to `reviewer_decide(advance:false)`.
|
||||
|
||||
Then replace Phase 4 step 5 (`5. Write schema_linking.json (the Phase 4 artifact) and close with reviewer_confirm kind:"phase". Do NOT run tht session check (that's Phase 5).`):
|
||||
|
||||
```markdown
|
||||
5. Persist `schema_linking.json` with the gate's `write_schema_linking` tool — it validates
|
||||
the object against the `SchemaLinking` model and writes the file deterministically (never
|
||||
hand-write it, never edit it with the file tool; on a validation error the tool returns the
|
||||
exact problem to fix). Shape: `{question, candidates:[{kind:"table"|"column", name,
|
||||
evidence?, decision?:"promoted"|"excluded"|"pending"}], joins:[{from, to, source?}],
|
||||
excluded:[{kind, name}], open_questions:[], concept_formulas:[]}`. Then close with
|
||||
`reviewer_confirm kind:"phase"`. Do NOT run `tht session check` (that's Phase 5).
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Clarify Phase 2 close (substantive memory needs the gate)**
|
||||
|
||||
In Phase 2, replace step 4 (`4. If no memory clears score 0.5, say so and close the phase quickly (reviewer_confirm kind:"phase" if the list is empty).`):
|
||||
|
||||
```markdown
|
||||
4. Closing: if memories were applied or rejected (substantive decisions), `advance:true`
|
||||
no-ops — close with `reviewer_confirm kind:"phase"`. Only a truly empty memory phase
|
||||
(nothing applied, nothing rejected) auto-advances via `advance:true`.
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Verify the edits read consistently**
|
||||
|
||||
Run: `cd harness && grep -n "advance:true\|reviewer_confirm kind:\"phase\"\|write_schema_linking\|Phase map" .pi/skills/tht-sessione/SKILL.md`
|
||||
Expected: the cheat-sheet section present; Phase 3 and Phase 4 now reference `reviewer_confirm kind:"phase"`; no remaining claim that a `reviewer_decide` "already advances" outside F2/F6. Read the four edited sections once to confirm no dangling references.
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add harness/.pi/skills/tht-sessione/SKILL.md
|
||||
git commit -m "docs(skill): correct phase-advance contract + add phase cheat-sheet
|
||||
|
||||
F3/F4 close with reviewer_confirm kind:'phase' (they do NOT auto-advance);
|
||||
advance:true only auto-advances F2-empty/F6-skip; rewrite_question belongs
|
||||
to F3 not F1; F4 uses the new write_schema_linking tool with the documented
|
||||
SchemaLinking shape. Adds a per-phase artifact/close cheat-sheet.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Full-suite verification + branch wrap-up
|
||||
|
||||
**Files:** none (verification only).
|
||||
|
||||
- [ ] **Step 1: Run the whole harness suite + lint**
|
||||
|
||||
Run: `cd harness && .venv/bin/pytest -q && .venv/bin/ruff check .`
|
||||
Expected: all green (prior 269 + the new `cte next` (3) + `set_schema_linking` store (2) + CLI (4) tests), ruff clean. If a pre-existing L2/L0 test skips (no VPN/Docker), that's expected — only unexpected failures block.
|
||||
|
||||
- [ ] **Step 2: Run the gate JS suite**
|
||||
|
||||
Run: `cd harness && node --test .pi/extensions/gate/__tests__/`
|
||||
Expected: existing gate unit suite green (no regressions from Tasks 2 and 5).
|
||||
|
||||
- [ ] **Step 3: Report status**
|
||||
|
||||
Summarize what shipped, and flag the DEFERRED live verification (needs VPN): drive the real stack through F4 (`write_schema_linking`) and F6 (approve a CTE → F6 closes) end-to-end. Note this closes the F6 dead-end found in session `2026-06-30-165708`. Do NOT claim live-verified — say "unit-verified; live check pending VPN".
|
||||
|
||||
## Self-Review
|
||||
|
||||
**Spec coverage:** Part 2 → Tasks 1-2 (cte next + gate by-name). Part 3 → Tasks 3-5 (store + CLI + gate tool). Part 1 → Task 6 (F3, Discipline 2, rewrite_question placement, F4 tool + shape, cheat-sheet, F2 close). Verification → Task 7. All spec sections mapped.
|
||||
|
||||
**Placeholder scan:** every code/test step contains full code; every run step has an exact command + expected result. No TBD/TODO.
|
||||
|
||||
**Type consistency:** `set_schema_linking(session_id, data, sessions_root) -> Path` defined in Task 3, consumed identically in Task 4. `tht cte next --session <id>` produced in Task 1, consumed in Task 2. `tht()` third arg `input` added in Task 5 Step 1 before first use in Step 2. Gate tool params `{session, schema_linking}` match the CLI `--file -` stdin contract.
|
||||
Reference in New Issue
Block a user