Files
ThothII/harness/tht/taskdoc.py
T
marcopanandClaude Opus 4.8 c4d130828f fix(harness): remediation difetti review — gate↔CLI, D15, D7/D6, D14, robustezza
Implementazione del piano di remediation progressiva sui difetti emersi
dall'analisi dell'harness. Tutto verificato: 214 test Python (incl. L0 su
Postgres reale), 14 test JS del gate, ruff pulito.

Blocco 1 (CRITICA, integrazione gate↔CLI):
- phase advance: gate usa --auto + exit 6; reviewer_confirm kind:phase fa
  advance esplicito che applica i prerequisiti (prima non avanzava per le
  fasi a conferma umana).
- cte plan riceve i --name dal gate (param names); set-question con id
  posizionale; skill `tht search find`; nuovo comando `tht memory save-one`
  con dedup hash client-side in save_one_memory.

Blocco 2 (D15, stato post-rollback):
- campo `phase` su DecisionRecord + effective_decisions phase-aware per i
  subject "a nome" (cte_approved ecc.); _compute_promotions e finalize sulla
  vista effective; finalize confronta col piano CTE effettivo, non glob;
  `decision add --retracts` + comando `decision retract`.

Blocco 3 (D7 read-only + D6 manifest):
- assert_read_only su tutti e quattro i codepath (direct + REST);
- manifest author/summary/updated_at/updated_by/schema_version popolati +
  helper touch_manifest sulle mutazioni.

Blocco 4-5 (D14a/D14b):
- decision_min_phase data-driven via `emits:` in workflow.yaml;
- formula evidence: status auto, search_formulas, gruppo CLI `tht formula`,
  `search find --kind formula`, load_evidence_dir salta i .sql.md.

Blocco 6 (robustezza):
- taskdoc slice promoted_tables + bound enforced; report escaping/bound +
  rsplit note; filtro kind reader REST/direct; conteggio upserted robusto;
  guard REST run_query non-list; LSH disallineato -> LshIndexError.

Blocco 7 (pulizia):
- dead code gate e KIND_TO_TABLE morto rimossi; doc Postgres-only
  (README + connection.py).

Blocco 0 (parziale): test di compatibilità firma gate↔CLI
(tests/integration). Rinviati: fake-Pi runtime completo, artifact-gate da
disco (#23), parità eligibility REST/direct (#28), unificazione
reserved-labels (#30), memory_rejected da deselezione (#33).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 17:16:51 +02:00

101 lines
3.7 KiB
Python

"""Per-step task document generator (spec D16, §4.9).
Emette un singolo documento compatto per fase/step, derivato dagli artefatti precedenti
e dalla vista effective delle decisioni, con byte budget enforced (target <20k token
per un modello 35B/<200k). MAI incorpora physical.yaml (~190k token, fatale).
D15+D16 complementari: il task doc e' generato dalla vista effective_decisions, quindi
post-rollback riflette automaticamente lo stato corretto (le decisioni stale di fasi
> current_phase sono escluse).
"""
from __future__ import annotations
import json
from dataclasses import dataclass
from pathlib import Path
from tht.phase import effective_decisions
from tht.workflow import load_workflow
MAX_BODY_BYTES = 80_000 # ~20k token (target per task document di una fase)
_TRUNCATION_MARKER = "\n\n[...troncato per il budget di contesto D16...]"
@dataclass
class TaskDoc:
phase: int
body: str
byte_budget_ok: bool
def _slice_schema_linking(raw: str, promoted_tables: list[str] | None) -> str:
"""Riduce schema_linking.json alle sole tabelle promosse (D16 §4.9: 'solo le
tabelle/colonne promosse, non tutto lo schema'). Senza promoted_tables passa il
contenuto invariato (e' gia' la superficie decisionale, non lo schema fisico).
Su JSON malformato ritorna il raw (il bound piu' sotto lo tronca se enorme)."""
if not promoted_tables:
return raw
try:
data = json.loads(raw)
except json.JSONDecodeError:
return raw
allow = set(promoted_tables)
def table_of(cand: dict) -> str:
return str(cand.get("name", "")).split(".")[0]
if isinstance(data, dict) and isinstance(data.get("candidates"), list):
data["candidates"] = [c for c in data["candidates"] if table_of(c) in allow]
return json.dumps(data, ensure_ascii=False, indent=2)
def generate_task_doc(
session_dir: Path | str,
phase: int,
promoted_tables: list[str] | None = None,
) -> TaskDoc:
"""Genera il documento di task per la fase `phase`.
Contenuto (compatti, mai artefatti integrali fatali):
- Domanda (question.md) se presente.
- Schema linking (schema_linking.json) solo da fase >= 4.
- Brief delle decisioni effective (esclude stale post-rollback, esclude ritirate).
- Header del task con il numero/nome della fase.
"""
session_dir = Path(session_dir)
parts: list[str] = []
q = session_dir / "question.md"
if q.exists():
parts.append("## Domanda\n" + q.read_text())
sl = session_dir / "schema_linking.json"
if sl.exists() and phase >= 4:
sliced = _slice_schema_linking(sl.read_text(), promoted_tables)
parts.append("## Schema linking (deciso)\n```json\n" + sliced + "\n```")
# Brief decisioni effective (D15-aware)
eff = effective_decisions(session_dir)
if eff:
lines = [f"- {d.type} | {d.subject} | {d.detail}" for d in eff]
parts.append("## Decisioni effettive (effective)\n" + "\n".join(lines))
# Header fase
try:
wf = load_workflow()
name = wf.phase_name(phase)
header = f"## Task: fase {phase} ({name})"
except Exception:
header = f"## Task: fase {phase}"
parts.append(header)
body = "\n\n".join(parts)
# Enforcement del bound (D16): non solo segnalare -- troncare. Un task doc oltre
# budget collasserebbe un 35B; meglio un documento troncato e marcato.
encoded = body.encode()
budget = MAX_BODY_BYTES - len(_TRUNCATION_MARKER.encode())
if len(encoded) > MAX_BODY_BYTES:
body = encoded[:budget].decode("utf-8", "ignore") + _TRUNCATION_MARKER
return TaskDoc(phase=phase, body=body, byte_budget_ok=False)
return TaskDoc(phase=phase, body=body, byte_budget_ok=True)