#!/usr/bin/env python3 """ STS Lab Srl — Spec-Driven Reader · spec_index.py Copyright (c) 2026 STS Lab Srl. Tutti i diritti riservati. Strumento della skill «sts-spec-driven» per leggere e scrivere documentazione spec-driven. Lettura (non scrive nulla): overview Che forma ha la documentazione? Quali spec, in che stato? find Dove è definito un ID e chi lo cita? (REQ-, SPEC-, T-, D-, DIF-) context Definizione, ID collegati a un passo, righe di TODO e TRACEABILITY. check Coerenza: ID orfani, spec senza criteri/task, task fuori dal TODO, spec non approvate con lavoro già fatto, segnaposto da compilare. next-id Prossimo ID libero: REQ (con --area), SPEC, D, DIF, T (con --spec o --prefisso). Scrittura (non sovrascrive mai un file esistente): scaffold --layout strutturato|piatto --progetto NOME Crea lo scheletro della documentazione dai modelli. new-spec --titolo "…" Crea una nuova spec (strutturato: 03-specs/SPEC-nnn-slug.md; piatto: _MODULE.md, serve --prefisso). Solo libreria standard Python 3.8+. """ from __future__ import annotations import argparse import datetime as dt import re import shutil import sys import unicodedata from collections import defaultdict from pathlib import Path BRAND = "STS Lab Srl · Spec-Driven Reader" SKILL_DIR = Path(__file__).resolve().parent.parent TEMPLATES = SKILL_DIR / "templates" LAYOUTS = ("strutturato", "piatto") ID_RE = re.compile( r"(? None: print(text) # ---------------------------------------------------------------- caricamento def load(docs: Path) -> dict[Path, list[str]]: files: dict[Path, list[str]] = {} for p in sorted(docs.rglob("*")): if p.is_file() and p.suffix.lower() in {".md", ".yaml", ".yml"}: try: files[p] = p.read_text(encoding="utf-8-sig", errors="replace").splitlines() except OSError: continue return files def strip_comments(lines: list[str]) -> list[str]: """Svuota i commenti HTML (, anche su più righe) mantenendo i numeri di riga: i segnaposto dei modelli non devono contare come ID.""" res, inside = [], False for line in lines: buf, i = "", 0 while i < len(line): if inside: j = line.find("-->", i) if j < 0: i = len(line) else: inside, i = False, j + 3 else: j = line.find("