perf(f1): cache-guard schema introspect + F1 toolbox in skill (~-5 min per session)

Transcript analysis (session 2026-07-06-175012, GLM 5.2) showed F1 at 567s:
182s wasted on a useless `tht schema introspect` (re-introspecting the remote
DWH although physical.yaml was already materialized) plus ~220s of model
thinking inflated by ~7 exploratory turns (--help/find/cat). The actual
searches cost ~15s; reviewer gates (~145s, untouched) are the quality contract.

- schema_cmd.py: introspect now exits 0 with "OK (cache)" in ~1s when
  physical.yaml exists; --refresh forces the real re-introspection.
  Deterministic cross-model guarantee, verified live on psd (163 tables, 1.2s).
- SKILL.md: F1 toolbox (only `tht search find` + `tht schema render`; no
  introspect/--help/filesystem browsing; batch all searches in one turn);
  F4 step 1 is render-only with a one-shot introspect fallback.
- tht-gate.js: `tht schema introspect ... --refresh` added to FORBIDDEN
  (maintenance stays shell-only, never in-session).
- tests: 4 new pytest cases (cache hit placement proven with fake credentials,
  refresh bypass, corrupt-catalog fall-through, render fallback message) and
  2 gate anti-bypass JS cases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 16:02:45 +02:00
co-authored by Claude Fable 5
parent 0cad04a8d8
commit 34aeda006e
5 changed files with 147 additions and 5 deletions
@@ -0,0 +1,27 @@
const test = require("node:test");
const assert = require("node:assert");
const { createFakePi } = require("./fake_pi_runtime.js");
const installGatePromise = import("../../tht-gate.js").then((m) => m.default);
test("tht schema introspect --refresh e' bloccato in sessione (manutenzione)", async () => {
const installGate = await installGatePromise;
const { pi } = createFakePi();
installGate(pi);
const res = await pi.emit("tool_call", {
toolName: "bash",
input: { command: "tht schema introspect -c workspaces/psd.yaml --refresh" },
});
assert.equal(res?.block, true);
});
test("tht schema introspect senza --refresh passa (cache hit innocuo)", async () => {
const installGate = await installGatePromise;
const { pi } = createFakePi();
installGate(pi);
const res = await pi.emit("tool_call", {
toolName: "bash",
input: { command: "tht schema introspect -c workspaces/psd.yaml" },
});
assert.equal(res, undefined);
});
+2
View File
@@ -109,6 +109,8 @@ const FORBIDDEN = [
/\btht\s+cte\s+plan\b/, /\btht\s+cte\s+plan\b/,
// La promozione in memoria passa dal reviewer (reviewer_memory_promote), mai da shell. // La promozione in memoria passa dal reviewer (reviewer_memory_promote), mai da shell.
/\btht\s+memory\s+(promote|save-one)\b/, /\btht\s+memory\s+(promote|save-one)\b/,
// La re-introspezione del DWH (~3 min) è manutenzione fuori sessione, mai in workflow.
/\btht\s+schema\s+introspect\b[^\n]*--refresh/,
]; ];
const PROTECTED_FILES = const PROTECTED_FILES =
/(review_decisions\.jsonl|session_manifest\.yaml|cte_plan\.json)/; /(review_decisions\.jsonl|session_manifest\.yaml|cte_plan\.json)/;
+15 -2
View File
@@ -148,10 +148,21 @@ it is complete. (The backend already refuses resume for finalized/archived sessi
Prerequisite: you must already be in Phase 1. Prerequisite: you must already be in Phase 1.
**F1 toolbox.** The only commands you need here are `tht search find` and `tht schema
render` — both fast, read-only lookups over workspace artifacts already on disk.
Evidence lives in `<workspace>/evidence/**` and is what `tht search find --kind evidence`
returns — do not browse it with `find`/`cat`. Do NOT run `tht schema introspect`: it is
a maintenance command that re-reads the remote DWH (~3 minutes); the catalog
`artifacts/mschema/physical.yaml` is already in the workspace. Do NOT explore with
`--help` or ad-hoc shell commands — every command you need is named in this skill.
1. Explore the DWH and knowledge base: `tht search find "<term>"` (evidence + schema, LSH 1. Explore the DWH and knowledge base: `tht search find "<term>"` (evidence + schema, LSH
over real values) and `tht search find --kind evidence "<term>"`. The LSH exposes over real values) and `tht search find --kind evidence "<term>"`. The LSH exposes
EVERY column where a value appears — it does not collapse to a single best match, EVERY column where a value appears — it does not collapse to a single best match,
so a value like "ablazione" may anchor on multiple columns. so a value like "ablazione" may anchor on multiple columns.
First list ALL the ambiguous terms in the question, then run the `tht search find`
calls for every term in ONE batch (a single message with multiple shell invocations)
— not one lookup per turn.
2. For each ambiguity (clinical term, population, time window, outcome), present the 2. For each ambiguity (clinical term, population, time window, outcome), present the
candidate interpretations (`recommended:true` on the best) + "Altro". Pick the widget candidate interpretations (`recommended:true` on the best) + "Altro". Pick the widget
by the question's shape: by the question's shape:
@@ -230,8 +241,10 @@ Phase 3 (CLI exit 5).
Prerequisite: Phase 3 closed. Prerequisite: Phase 3 closed.
1. `tht schema introspect` + `tht schema render --format mschema-text` for the schema 1. `tht schema render --format mschema-text` for the schema context (the catalog
context. Copy table/column names EXACTLY from it — never invent objects. `artifacts/mschema/physical.yaml` is already in the workspace; only if render fails
with `physical.yaml non trovato`, run `tht schema introspect` once, then render).
Copy table/column names EXACTLY from it — never invent objects.
Also run `tht memory solved-search "<question>" --json`: similar already-solved Also run `tht memory solved-search "<question>" --json`: similar already-solved
questions show which tables comparable questions used. Cite relevant precedents questions show which tables comparable questions used. Cite relevant precedents
(session id + tables) to the reviewer as CONTEXT — they are reference material, (session id + tables) to the reviewer as CONTEXT — they are reference material,
@@ -0,0 +1,70 @@
from datetime import datetime
from typer.testing import CliRunner
from tht.cli import app
from tht.mschema.models import ColumnPhysical, PhysicalSchema, TablePhysical
def _write_catalog(tmp_path):
phys = PhysicalSchema(
database="d", schema="s", introspected_at=datetime(2026, 1, 1),
tables={
"dim_patient": TablePhysical(
comment="Anagrafica",
columns={
"cod_paz": ColumnPhysical(type="bigint", pk=True, comment="Codice paziente"),
},
)
},
)
out = tmp_path / "artifacts" / "mschema" / "physical.yaml"
phys.to_yaml(out)
return out
def _write_config(tmp_path):
cfg = tmp_path / "workspace.yaml"
cfg.write_text(
"database: {database: d, schema: s, user: u, password: p, transport: direct}\n"
f"paths: {{artifacts: {tmp_path/'artifacts'}, indexes: {tmp_path/'i'}, sessions: {tmp_path/'s'}}}\n"
)
return cfg
def test_introspect_cache_hit_skips_dwh(tmp_path):
# Le credenziali sono fasulle: se la guardia non scattasse PRIMA del branch
# transport, il comando tenterebbe la connessione e fallirebbe.
catalog = _write_catalog(tmp_path)
before = catalog.read_bytes()
cfg = _write_config(tmp_path)
res = CliRunner().invoke(app, ["schema", "introspect", "-c", str(cfg)])
assert res.exit_code == 0, res.output
assert "OK (cache)" in res.output
assert "1 tabelle" in res.output
assert catalog.read_bytes() == before
def test_introspect_refresh_bypasses_cache(tmp_path):
_write_catalog(tmp_path)
cfg = _write_config(tmp_path)
res = CliRunner().invoke(app, ["schema", "introspect", "-c", str(cfg), "--refresh"])
assert res.exit_code != 0
assert "OK (cache)" not in res.output
def test_introspect_corrupt_catalog_falls_through(tmp_path):
catalog = tmp_path / "artifacts" / "mschema" / "physical.yaml"
catalog.parent.mkdir(parents=True)
catalog.write_text("{not: [valid")
cfg = _write_config(tmp_path)
res = CliRunner().invoke(app, ["schema", "introspect", "-c", str(cfg)])
assert res.exit_code != 0
assert "OK (cache)" not in res.output
def test_render_without_catalog_guides_fallback(tmp_path):
cfg = _write_config(tmp_path)
res = CliRunner().invoke(app, ["schema", "render", "-c", str(cfg)])
assert res.exit_code == 1
assert "Esegui prima" in res.output
+33 -3
View File
@@ -30,9 +30,40 @@ def annotations_path(cfg) -> Path:
@schema_app.command("introspect") @schema_app.command("introspect")
def introspect_cmd(config: Path = CONFIG_OPT) -> None: def introspect_cmd(
"""Introspeziona lo schema target e genera artifacts/mschema/physical.yaml.""" config: Path = CONFIG_OPT,
refresh: bool = typer.Option(
False,
"--refresh",
help="Forza la re-introspezione del DWH anche se physical.yaml esiste già.",
),
) -> None:
"""Introspeziona lo schema target e genera artifacts/mschema/physical.yaml.
Se physical.yaml esiste già, esce subito (cache); usa --refresh per rigenerarlo.
"""
cfg = _load_config_or_exit(config) cfg = _load_config_or_exit(config)
out = physical_path(cfg)
if out.exists() and not refresh:
from datetime import UTC, datetime
from tht.mschema.models import PhysicalSchema
try:
cached = PhysicalSchema.from_yaml(out)
except Exception:
pass # catalogo illeggibile: procedi con la re-introspezione
else:
ts = cached.introspected_at
if ts.tzinfo is None:
ts = ts.replace(tzinfo=UTC)
age_days = (datetime.now(UTC) - ts).days
typer.secho(
f"OK (cache): {out} esistente ({len(cached.tables)} tabelle, "
f"età {age_days}g). Re-introspezione solo con --refresh (manutenzione).",
fg=typer.colors.GREEN,
)
return
if cfg.database.transport == "rest": if cfg.database.transport == "rest":
from tht.db.introspect import introspect_rest from tht.db.introspect import introspect_rest
from tht.db.sampling import add_examples_rest from tht.db.sampling import add_examples_rest
@@ -59,7 +90,6 @@ def introspect_cmd(config: Path = CONFIG_OPT) -> None:
except (OperationalError, IntrospectionError) as e: except (OperationalError, IntrospectionError) as e:
typer.secho(f"ERRORE: {e}", fg=typer.colors.RED, err=True) typer.secho(f"ERRORE: {e}", fg=typer.colors.RED, err=True)
raise typer.Exit(code=1) raise typer.Exit(code=1)
out = physical_path(cfg)
phys.to_yaml(out) phys.to_yaml(out)
n_cols = sum(len(t.columns) for t in phys.tables.values()) n_cols = sum(len(t.columns) for t in phys.tables.values())
n_ignored = sum( n_ignored = sum(