Files
ThothII/harness/tht/lshindex/__init__.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

90 lines
2.9 KiB
Python

import json
import pickle
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from datasketch import MinHash, MinHashLSH
from tht.config import LshConfig
from tht.vendor.thoth_lsh import create_lsh_index, create_minhash
class LshIndexError(Exception):
pass
@dataclass
class LshHit:
table: str
column: str
value: str
score: float
def build_index(
values: dict[str, dict[str, list[str]]], cfg: LshConfig, verbose: bool = False
) -> tuple[MinHashLSH, dict[str, tuple[MinHash, str, str, str]]]:
return create_lsh_index(
values, signature_size=cfg.signature_size, n_gram=cfg.n_gram,
threshold=cfg.threshold, verbose=verbose,
)
def _paths(directory: Path, name: str) -> tuple[Path, Path, Path]:
return (
directory / f"{name}_lsh.pkl",
directory / f"{name}_minhashes.pkl",
directory / f"{name}_meta.json",
)
def save_index(lsh, minhashes, cfg: LshConfig, directory: Path, name: str) -> None:
directory.mkdir(parents=True, exist_ok=True)
lsh_path, mh_path, meta_path = _paths(directory, name)
lsh_path.write_bytes(pickle.dumps(lsh))
mh_path.write_bytes(pickle.dumps(minhashes))
meta_path.write_text(
json.dumps(
{"signature_size": cfg.signature_size, "n_gram": cfg.n_gram,
"threshold": cfg.threshold, "entries": len(minhashes)},
indent=2,
)
)
def load_index(directory: Path, name: str) -> tuple[Any, Any, dict]:
lsh_path, mh_path, meta_path = _paths(directory, name)
if not (lsh_path.exists() and mh_path.exists() and meta_path.exists()):
raise LshIndexError(
f"Indice LSH non trovato in {directory} (atteso {name}_lsh.pkl). "
f"Esegui prima `tht lsh build`."
)
lsh = pickle.loads(lsh_path.read_bytes())
minhashes = pickle.loads(mh_path.read_bytes())
meta = json.loads(meta_path.read_text())
return lsh, minhashes, meta
def query_index(lsh, minhashes, keyword: str, meta: dict, top_n: int = 10) -> list[LshHit]:
"""Query con score: i parametri MinHash vengono dal meta dell'indice, non dalla config.
Se i due pickle (`*_lsh.pkl` e `*_minhashes.pkl`) sono disallineati (rigenerati
separatamente) `lsh.query` puo' restituire chiavi assenti da `minhashes`: si solleva
LshIndexError azionabile invece di un KeyError opaco."""
qmh = create_minhash(meta["signature_size"], keyword, meta["n_gram"])
scored = []
for key in lsh.query(qmh):
entry = minhashes.get(key)
if entry is None:
raise LshIndexError(
"Indice LSH disallineato (lsh.pkl e minhashes.pkl non coerenti): "
"rigenera con `tht lsh build`."
)
scored.append((key, qmh.jaccard(entry[0])))
scored.sort(key=lambda kv: kv[1], reverse=True)
return [
LshHit(table=minhashes[k][1], column=minhashes[k][2], value=minhashes[k][3], score=s)
for k, s in scored[:top_n]
]