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:
@@ -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);
|
||||
});
|
||||
@@ -109,6 +109,8 @@ const FORBIDDEN = [
|
||||
/\btht\s+cte\s+plan\b/,
|
||||
// La promozione in memoria passa dal reviewer (reviewer_memory_promote), mai da shell.
|
||||
/\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 =
|
||||
/(review_decisions\.jsonl|session_manifest\.yaml|cte_plan\.json)/;
|
||||
|
||||
@@ -148,10 +148,21 @@ it is complete. (The backend already refuses resume for finalized/archived sessi
|
||||
|
||||
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
|
||||
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,
|
||||
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
|
||||
candidate interpretations (`recommended:true` on the best) + "Altro". Pick the widget
|
||||
by the question's shape:
|
||||
@@ -230,8 +241,10 @@ Phase 3 (CLI exit 5).
|
||||
|
||||
Prerequisite: Phase 3 closed.
|
||||
|
||||
1. `tht schema introspect` + `tht schema render --format mschema-text` for the schema
|
||||
context. Copy table/column names EXACTLY from it — never invent objects.
|
||||
1. `tht schema render --format mschema-text` for the schema context (the catalog
|
||||
`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
|
||||
questions show which tables comparable questions used. Cite relevant precedents
|
||||
(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
|
||||
@@ -30,9 +30,40 @@ def annotations_path(cfg) -> Path:
|
||||
|
||||
|
||||
@schema_app.command("introspect")
|
||||
def introspect_cmd(config: Path = CONFIG_OPT) -> None:
|
||||
"""Introspeziona lo schema target e genera artifacts/mschema/physical.yaml."""
|
||||
def introspect_cmd(
|
||||
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)
|
||||
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":
|
||||
from tht.db.introspect import introspect_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:
|
||||
typer.secho(f"ERRORE: {e}", fg=typer.colors.RED, err=True)
|
||||
raise typer.Exit(code=1)
|
||||
out = physical_path(cfg)
|
||||
phys.to_yaml(out)
|
||||
n_cols = sum(len(t.columns) for t in phys.tables.values())
|
||||
n_ignored = sum(
|
||||
|
||||
Reference in New Issue
Block a user