"""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 from dataclasses import dataclass from pathlib import Path from nsp.phase import effective_decisions from nsp.workflow import load_workflow MAX_BODY_BYTES = 80_000 # ~20k token (target per task document di una fase) @dataclass class TaskDoc: phase: int body: str byte_budget_ok: bool 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: parts.append("## Schema linking (deciso)\n```json\n" + sl.read_text() + "\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) return TaskDoc(phase=phase, body=body, byte_budget_ok=len(body.encode()) <= MAX_BODY_BYTES)