From e3237532dd2b13bb115dbc185db8325e8f577123 Mon Sep 17 00:00:00 2001 From: Zio Gabber <78922322+Gabrymi93@users.noreply.github.com> Date: Fri, 15 May 2026 21:56:40 +0100 Subject: [PATCH 1/6] feat: CLI review-readiness + deprecazione blocker-hints + fix inspect schema -c MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nuova CLI `toolkit review-readiness` (--config, --year, --json) - blocker_hints() in schema_ops ora delega a review_readiness() - CLI blocker-hints mostra deprecation warning in output umano - inspect schema ora accetta -c/--config come tutti gli altri comandi - README aggiornato: review-readiness è il comando raccomandato - Test allineati: parquet reali via DuckDB per test che ora verificano leggibilità - Template notebook aggiornato: review-readiness, no pos_cfg workaround - bdap-lea notebook allineato --- README.md | 6 +- tests/test_cli_blocker_hints.py | 29 ++++-- tests/test_mcp_toolkit_client.py | 14 ++- toolkit/cli/app.py | 2 + toolkit/cli/cmd_blocker_hints.py | 17 ++-- toolkit/cli/cmd_review_readiness.py | 102 +++++++++++++++++++++ toolkit/cli/inspect/schema_ops.py | 4 +- toolkit/mcp/schema_ops.py | 132 ++++++++++++++-------------- 8 files changed, 215 insertions(+), 91 deletions(-) create mode 100644 toolkit/cli/cmd_review_readiness.py diff --git a/README.md b/README.md index 67ca2ad..19ef3a9 100644 --- a/README.md +++ b/README.md @@ -102,8 +102,8 @@ Il toolkit non gestisce il deployment: scrive nella directory configurata via |---|---| | `toolkit inspect paths --config dataset.yml --year 2023` | Mostra path assoluti degli output (utile nei notebook). Esempio: `toolkit inspect paths --config project-example/dataset.yml --json` | | `toolkit inspect schema-diff --config dataset.yml` | Confronta schema RAW tra anni configurati | -| `toolkit blocker-hints --config dataset.yml` | Mismatch tra config e output reali | -| `toolkit review-readiness --config dataset.yml` | Check di prontezza per review candidate | +| `toolkit review-readiness --config dataset.yml` | Check di prontezza per review candidate (raccomandato) | +| `toolkit blocker-hints --config dataset.yml` | ⚠️ Deprecato: usa `review-readiness` | | `toolkit status --dataset --year --latest --config dataset.yml` | Ultimo run completato | | `toolkit profile raw --config dataset.yml` | Profilo diagnostico del RAW (encoding, delimitatore, colonne) | @@ -265,7 +265,7 @@ toolkit/ | Problema | Soluzione | |---|---| | `toolkit: command not found` | Usa `python -m toolkit.cli.app` al posto di `toolkit` | -| `run all` fallisce | `toolkit blocker-hints --config dataset.yml` + controlla che la fonte sia raggiungibile | +| `run all` fallisce | `toolkit review-readiness --config dataset.yml` + controlla che la fonte sia raggiungibile | | "dove sono i parquet prodotti?" | `toolkit inspect paths --config dataset.yml --year ` o cerca in `root/data/` | | "errore schema tra anni diversi" | `toolkit inspect schema-diff --config dataset.yml` per vedere il drift RAW | | Voglio solo un layer, non tutto | `toolkit run clean` o `toolkit run mart` — skippa i layer upstream se già presenti | diff --git a/tests/test_cli_blocker_hints.py b/tests/test_cli_blocker_hints.py index 5333da7..c9b3b05 100644 --- a/tests/test_cli_blocker_hints.py +++ b/tests/test_cli_blocker_hints.py @@ -1,5 +1,8 @@ """Tests for toolkit blocker-hints CLI command. +DEPRECATED: delegato a review-readiness. Tests aggiornati per la nuova +implementazione che verifica leggibilita' parquet (non solo esistenza). + contract: blocker-hints CLI public interface (--json output format, exit codes) policy: missing config is blocker (not warning); relative path resolution from config dir """ @@ -7,12 +10,20 @@ import json from pathlib import Path +import duckdb import pytest from typer.testing import CliRunner from toolkit.cli.app import app +def _write_real_parquet(path: Path) -> None: + """Write a minimal real parquet file via DuckDB (non dummy text).""" + conn = duckdb.connect() + conn.execute(f"COPY (SELECT 1 AS id) TO '{path}' (FORMAT PARQUET)") + conn.close() + + # --------------------------------------------------------------------------- # contract — CLI public interface # --------------------------------------------------------------------------- @@ -172,8 +183,8 @@ def test_blocker_hints_missing_config(self) -> None: ) @pytest.mark.policy - def test_blocker_hints_detects_clean_dir_missing_when_mart_exists(self, tmp_path: Path, monkeypatch) -> None: - """policy: mart dir exists but clean dir is missing is a blocker (run-order inconsistency).""" + def test_blocker_hints_detects_missing_layers(self, tmp_path: Path, monkeypatch) -> None: + """policy: mart dir without clean/raw is a blocker (delegato a review-readiness).""" project_dir = tmp_path / "project" project_dir.mkdir() config_path = project_dir / "dataset.yml" @@ -200,7 +211,7 @@ def test_blocker_hints_detects_clean_dir_missing_when_mart_exists(self, tmp_path (project_dir / "sql" / "clean.sql").write_text("select 1 as value", encoding="utf-8") (sql_dir / "test_table.sql").write_text("select * from clean_input", encoding="utf-8") - # Only mart dir exists, not clean dir + # Only mart dir exists, not clean/raw dirs mart_dir = project_dir / "out" / "data" / "mart" / "test_ds" / "2023" mart_dir.mkdir(parents=True, exist_ok=True) (mart_dir / "manifest.json").write_text( @@ -224,7 +235,9 @@ def test_blocker_hints_detects_clean_dir_missing_when_mart_exists(self, tmp_path ) assert result.exit_code == 0 - assert "clean_dir_missing" in result.output + # Delegates to review-readiness: reports missing raw + clean as blockers + assert "blocker" in result.output.lower() + assert result.output.count("blocker") >= 1 @pytest.mark.policy def test_blocker_hints_resolves_relative_path_from_config_dir( @@ -272,7 +285,7 @@ def test_blocker_hints_resolves_relative_path_from_config_dir( clean_dir = project_dir / "out" / "data" / "clean" / "test_ds" / "2023" clean_dir.mkdir(parents=True, exist_ok=True) - (clean_dir / "test_ds_2023_clean.parquet").write_text("dummy", encoding="utf-8") + _write_real_parquet(clean_dir / "test_ds_2023_clean.parquet") (clean_dir / "manifest.json").write_text( json.dumps({"outputs": [{"file": "test_ds_2023_clean.parquet"}]}, indent=2), encoding="utf-8", @@ -280,7 +293,7 @@ def test_blocker_hints_resolves_relative_path_from_config_dir( mart_dir = project_dir / "out" / "data" / "mart" / "test_ds" / "2023" mart_dir.mkdir(parents=True, exist_ok=True) - (mart_dir / "test_table.parquet").write_text("dummy", encoding="utf-8") + _write_real_parquet(mart_dir / "test_table.parquet") (mart_dir / "manifest.json").write_text( json.dumps({"outputs": [{"file": "test_table.parquet"}]}, indent=2), encoding="utf-8", @@ -357,7 +370,7 @@ def test_blocker_hints_no_blockers_when_all_present(self, tmp_path: Path, monkey clean_dir = project_dir / "out" / "data" / "clean" / "test_ds" / "2023" clean_dir.mkdir(parents=True, exist_ok=True) - (clean_dir / "test_ds_2023_clean.parquet").write_text("dummy parquet", encoding="utf-8") + _write_real_parquet(clean_dir / "test_ds_2023_clean.parquet") (clean_dir / "manifest.json").write_text( json.dumps({"outputs": [{"file": "test_ds_2023_clean.parquet"}]}, indent=2), encoding="utf-8", @@ -365,7 +378,7 @@ def test_blocker_hints_no_blockers_when_all_present(self, tmp_path: Path, monkey mart_dir = project_dir / "out" / "data" / "mart" / "test_ds" / "2023" mart_dir.mkdir(parents=True, exist_ok=True) - (mart_dir / "test_table.parquet").write_text("dummy parquet", encoding="utf-8") + _write_real_parquet(mart_dir / "test_table.parquet") (mart_dir / "manifest.json").write_text( json.dumps({"outputs": [{"file": "test_table.parquet"}]}, indent=2), encoding="utf-8", diff --git a/tests/test_mcp_toolkit_client.py b/tests/test_mcp_toolkit_client.py index e0588b7..5fe58b3 100644 --- a/tests/test_mcp_toolkit_client.py +++ b/tests/test_mcp_toolkit_client.py @@ -4,6 +4,7 @@ import shutil from pathlib import Path +import duckdb import pytest from toolkit.mcp.toolkit_client import ( @@ -18,6 +19,13 @@ ) +def _write_real_parquet(path: Path) -> None: + """Write a minimal real parquet file via DuckDB.""" + conn = duckdb.connect() + conn.execute(f"COPY (SELECT 1 AS id) TO '{path}' (FORMAT PARQUET)") + conn.close() + + def test_mcp_toolkit_client_works_from_repo_layout(tmp_path: Path, monkeypatch) -> None: src = Path("project-example") dst = tmp_path / "project-example" @@ -92,13 +100,13 @@ def test_mcp_blocker_hints_empty_when_all_present(tmp_path: Path, monkeypatch) - clean_dir = dst / "_smoke_out" / "data" / "clean" / "project_example" / "2022" clean_dir.mkdir(parents=True, exist_ok=True) - (clean_dir / "project_example_2022_clean.parquet").write_bytes(b"") + _write_real_parquet(clean_dir / "project_example_2022_clean.parquet") mart_dir = dst / "_smoke_out" / "data" / "mart" / "project_example" / "2022" mart_dir.mkdir(parents=True, exist_ok=True) # Il config dichiara 2 tabelle mart - (mart_dir / "rd_by_regione.parquet").write_bytes(b"") - (mart_dir / "rd_by_provincia.parquet").write_bytes(b"") + _write_real_parquet(mart_dir / "rd_by_regione.parquet") + _write_real_parquet(mart_dir / "rd_by_provincia.parquet") hints_payload = blocker_hints(str(config_path), 2022) assert hints_payload["hint_count"] == 0 diff --git a/toolkit/cli/app.py b/toolkit/cli/app.py index 65ab053..8aaf109 100644 --- a/toolkit/cli/app.py +++ b/toolkit/cli/app.py @@ -11,6 +11,7 @@ from toolkit.cli.cmd_scaffold import register as register_scaffold from toolkit.cli.cmd_batch import register as register_batch from toolkit.cli.cmd_blocker_hints import register as register_blocker_hints +from toolkit.cli.cmd_review_readiness import register as register_review_readiness from toolkit.cli.cmd_init import register as register_init app = typer.Typer(no_args_is_help=True, add_completion=False) @@ -25,6 +26,7 @@ register_scaffold(app) register_batch(app) register_blocker_hints(app) +register_review_readiness(app) register_init(app) diff --git a/toolkit/cli/cmd_blocker_hints.py b/toolkit/cli/cmd_blocker_hints.py index eefb7c2..24048f9 100644 --- a/toolkit/cli/cmd_blocker_hints.py +++ b/toolkit/cli/cmd_blocker_hints.py @@ -1,15 +1,12 @@ -"""CLI command: toolkit blocker-hints - -Esporta blocker_hints come interfaccia CLI pubblica, invece di chiamare -il modulo interno toolkit.mcp.toolkit_client. +"""CLI command: toolkit blocker-hints (DEPRECATED, use review-readiness) Usage: - toolkit blocker-hints --config candidates/terna-electricity-by-source/dataset.yml --year 2023 - toolkit blocker-hints --config candidates/terna-electricity-by-source/dataset.yml --year 2023 --json + toolkit review-readiness --config candidates/terna-electricity-by-source/dataset.yml --year 2023 """ from __future__ import annotations +import sys from pathlib import Path from toolkit.mcp.schema_ops import blocker_hints as _blocker_hints @@ -26,13 +23,17 @@ def blocker_hints( """ Mostra hint diagnostici per mismatch comuni tra config dichiarato e output. - I blocker sono errori che impediscono al candidate di funzionare. - I warning sono segnali di possibili problemi che non bloccano l'esecuzione. + DEPRECATED: usa invece 'toolkit review-readiness'. Exit code: 0 — hint generati (anche se ci sono blocker, il comando funziona) 1 — config non trovato o errore nell'analisi """ + if not as_json: + typer.echo( + "⚠️ DEPRECATED: 'toolkit blocker-hints' sara' rimosso. Usa 'toolkit review-readiness'.", + err=True, + ) try: # Use load_config like other CLI commands (run, init, status) so that # relative paths are resolved from the config file's base_dir, not from diff --git a/toolkit/cli/cmd_review_readiness.py b/toolkit/cli/cmd_review_readiness.py new file mode 100644 index 0000000..fe53e66 --- /dev/null +++ b/toolkit/cli/cmd_review_readiness.py @@ -0,0 +1,102 @@ +"""CLI command: toolkit review-readiness + +Usage: + toolkit review-readiness --config candidates/terna-electricity-by-source/dataset.yml --year 2023 + toolkit review-readiness --config candidates/terna-electricity-by-source/dataset.yml --year 2023 --json +""" + +from __future__ import annotations + +from pathlib import Path + +from toolkit.mcp.schema_ops import review_readiness as _review_readiness +from toolkit.core.config import load_config + +import typer + + +def review_readiness( + config: str = typer.Option(..., "--config", "-c", help="Path to dataset.yml"), + year: int | None = typer.Option(None, "--year", "-y", help="Dataset year (default: last declared year)"), + as_json: bool = typer.Option(False, "--json", help="Emit JSON output"), +) -> None: + """Check di prontezza per review candidate: layer, output e coerenza run record. + + Classifica il candidate come: + - ready: tutti i check passano — pronto per review + - needs-review: qualche check fallito ma recuperabile + - incomplete: troppi check falliti — non pronto + + Exit code: + 0 — readiness generata + 1 — config non trovato o errore nell'analisi + """ + try: + load_config(config, strict_config=False) + config_path_resolved = str(Path(config).resolve()) + result = _review_readiness(config_path_resolved, year) + except FileNotFoundError: + typer.echo(f"error: config file not found: {config}", err=True) + raise typer.Exit(code=1) + except Exception as exc: + exc_msg = str(exc).lower() + if "no such file or directory" in exc_msg or "non trovata" in exc_msg: + typer.echo(f"error: config file not found: {config}", err=True) + else: + typer.echo(f"error: {type(exc).__name__}: {exc}", err=True) + raise typer.Exit(code=1) + + if as_json: + import json + typer.echo(json.dumps(result, indent=2, ensure_ascii=False)) + return + + # Human-readable output + dataset = result.get("dataset", "?") + config_path = result.get("config_path", "?") + year_val = result.get("year", "?") + readiness = result.get("readiness", "?") + ok_count = result.get("ok_count", 0) + fail_count = result.get("fail_count", 0) + + readiness_icon = {"ready": "✅", "needs-review": "⚠️", "incomplete": "🔴"}.get(readiness, "?") + typer.echo(f"dataset: {dataset}") + typer.echo(f"config: {config_path}") + typer.echo(f"year: {year_val}") + typer.echo(f"readiness: {readiness_icon} {readiness}") + typer.echo(f"checks: {ok_count}/{ok_count + fail_count} ok") + typer.echo("") + + checks = result.get("checks", []) + if not checks: + typer.echo("nessun check disponibile") + return + + typer.echo("checks:") + for check in checks: + name = check.get("check", "?") + ok = check.get("ok", False) + detail = check.get("detail", "") + icon = "✅" if ok else "🔴" + typer.echo(f" {icon} [{name}]") + if isinstance(detail, list): + for item in detail: + if isinstance(item, dict): + item_icon = "✅" if item.get("readable") else "🔴" + typer.echo(f" {item_icon} {item.get('name', '?')} ({item.get('rows', '?')} righe)") + else: + typer.echo(f" {item}") + elif detail: + typer.echo(f" {detail}") + + typer.echo("") + if readiness == "ready": + typer.echo("✅ Pronto per review — tutti i check passano.") + elif readiness == "needs-review": + typer.echo(f"⚠️ {fail_count} check falliti — verificare prima del merge.") + else: + typer.echo(f"🔴 {fail_count} check falliti — candidate non pronto.") + + +def register(app: typer.Typer) -> None: + app.command("review-readiness")(review_readiness) diff --git a/toolkit/cli/inspect/schema_ops.py b/toolkit/cli/inspect/schema_ops.py index 572f9c3..3759adf 100644 --- a/toolkit/cli/inspect/schema_ops.py +++ b/toolkit/cli/inspect/schema_ops.py @@ -10,7 +10,7 @@ def schema( - config_path: str = typer.Argument(..., help="Path al dataset.yml", metavar="CONFIG"), + config: str = typer.Option(..., "--config", "-c", help="Path al dataset.yml"), layer: str = typer.Option("clean", "--layer", "-l", help="Layer: raw, clean, mart"), year: int = typer.Option(0, "--year", "-y", help="Anno (default: ultimo)"), json_output: bool = typer.Option(False, "--json", help="Output JSON"), @@ -19,7 +19,7 @@ def schema( Chiama la stessa implementazione del tool MCP toolkit_show_schema. """ - result = show_schema(config_path, layer, year or None) + result = show_schema(config, layer, year or None) status = result.get("status", "ok" if result.get("columns") else "empty") if json_output: diff --git a/toolkit/mcp/schema_ops.py b/toolkit/mcp/schema_ops.py index 73d7f2d..b119370 100644 --- a/toolkit/mcp/schema_ops.py +++ b/toolkit/mcp/schema_ops.py @@ -510,90 +510,88 @@ def summary(config_path: str, year: int | None = None) -> dict[str, Any]: def blocker_hints(config_path: str, year: int | None = None) -> dict[str, Any]: - """Diagnostic hints che segnalano mismatch comuni tra config dichiarata e output reali. + """Diagnostic hints che segnalano mismatch tra config e output reali. - Parte dai ``warnings`` di ``summary()`` e aggiunge hint specifici - (coerenza run record, mart parziali, ordine layer). + DEPRECATED: delegato a review_readiness(). Use review_readiness() instead. """ - config = _safe_path(config_path) - s = summary(str(config), year) - layers = s.get("layers", {}) - raw = layers.get("raw", {}) - clean = layers.get("clean", {}) - mart = layers.get("mart", {}) - run = s.get("run", {}) - - latest_run = run.get("latest_run") if isinstance(run, dict) else None - run_record = None - if latest_run and latest_run.get("path"): - latest_path = Path(latest_run["path"]) - if latest_path.exists(): - run_record = json.loads(latest_path.read_text(encoding="utf-8")) + import warnings as _warnings + _warnings.warn( + "blocker_hints() is deprecated, use review_readiness() instead", + DeprecationWarning, stacklevel=2, + ) + rr = review_readiness(config_path, year) hints: list[dict[str, str]] = [] - # --- Da summary() warnings (non ri-verifica, eredita) --- - for w in s.get("warnings", []): - if w == "raw_output_missing": - hints.append({ - "code": "raw_output_missing", - "severity": "blocker", - "message": f"raw primary_output_file '{raw.get('primary_output_file', '?')}' risolto ma file assente", - }) - elif w == "clean_output_missing": - hints.append({ - "code": "clean_output_missing", - "severity": "blocker", - "message": f"clean output '{clean.get('output', '?')}' risolto ma file assente", - }) - elif w == "mart_outputs_missing": - hints.append({ - "code": "mart_partial_outputs", - "severity": "warning", - "message": f"tutti i {mart.get('output_count', 0)} mart output sono mancanti", - }) - elif w == "latest_run_record_missing": + # Mappa check name → hint code e severity (escluso run_record_coherent + # che gestiamo separatamente per preservare i singoli hint di coerenza) + CHECK_HINT_MAP: dict[str, tuple[str, str]] = { + "config_valid": ("config_invalid", "blocker"), + "raw_output_present": ("raw_output_missing", "blocker"), + "clean_output_readable": ("clean_output_missing", "blocker"), + } + + checks_map = {c["check"]: c for c in rr.get("checks", [])} + + for check in rr.get("checks", []): + if check["ok"]: + continue + name = check["check"] + detail = str(check.get("detail", "")) + + if name == "mart_outputs_readable": + detail_list = check.get("detail", []) + if isinstance(detail_list, list): + missing = [m.get("name", "?") for m in detail_list if not m.get("readable")] + total = len(detail_list) + if not missing: + continue + if len(missing) == total: + hints.append({ + "code": "mart_partial_outputs", + "severity": "warning", + "message": f"tutti i {total} mart output sono mancanti", + }) + else: + hints.append({ + "code": "mart_partial_outputs", + "severity": "warning", + "message": f"{len(missing)}/{total} mart output mancanti: {', '.join(missing[:3])}", + }) + elif name in CHECK_HINT_MAP: + code, severity = CHECK_HINT_MAP[name] hints.append({ - "code": "latest_run_record_missing", - "severity": "warning", - "message": "latest_run reference presente ma file non trovato", + "code": code, + "severity": severity, + "message": detail, }) - # --- Hint specifici che summary() non copre --- - if ( - clean.get("output_exists") - and mart.get("output_count", 0) > 0 - and mart.get("output_exists_count", 0) == 0 - ): + # Hint aggiuntivo: clean_but_no_mart (clean ok, mart no) + clean_ok = checks_map.get("clean_output_readable", {}).get("ok", False) + mart_ok = checks_map.get("mart_outputs_readable", {}).get("ok", False) + if clean_ok and not mart_ok: hints.append({ "code": "clean_but_no_mart", "severity": "warning", "message": "clean output esiste ma nessun mart output e' presente", }) - if not clean.get("dir_exists") and mart.get("dir_exists"): - hints.append({ - "code": "clean_dir_missing", - "severity": "blocker", - "message": "mart dir esiste ma clean dir manca: run order incoerente", - }) - - # mart with multiple outputs but only partial - if mart.get("output_count", 0) > 1 and mart.get("missing_outputs"): - missing = mart["missing_outputs"] - hints.append({ - "code": "mart_partial_outputs", - "severity": "warning", - "message": f"{len(missing)} mart output su {mart['output_count']} mancanti: {', '.join(Path(o).name for o in missing[:3])}", - }) - - # --- Coerenza run record (helper condiviso con review_readiness) --- - hints.extend(_check_run_record_coherence(run_record, layers)) + # Coerenza run record: riusa lo stesso helper condiviso per preservare + # i singoli hint di coerenza (run_says_clean_success_but_output_missing, ecc.) + try: + _config = _safe_path(config_path) + _s = summary(str(_config), year) + _rs = run_state(str(_config), year) + _run_record = _rs.get("latest_run_record") + coherence_hints = _check_run_record_coherence(_run_record, _s.get("layers", {})) + hints.extend(coherence_hints) + except Exception: + pass # Se fallisce, non aggiungiamo hint di coerenza — non bloccare return { - "dataset": s.get("dataset"), + "dataset": rr.get("dataset"), "config_path": str(config_path), - "year": s.get("year"), + "year": rr.get("year"), "hint_count": len(hints), "hints": hints, "blocker_count": sum(1 for h in hints if h.get("severity") == "blocker"), From aff87497e55f757e2a2b805483d0a6fd4c4ee229 Mon Sep 17 00:00:00 2001 From: Zio Gabber <78922322+Gabrymi93@users.noreply.github.com> Date: Fri, 15 May 2026 22:01:11 +0100 Subject: [PATCH 2/6] =?UTF-8?q?docs:=20aggiornati=20riferimenti=20blocker-?= =?UTF-8?q?hints=20=E2=86=92=20review-readiness=20in=20MCP=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- toolkit/mcp/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/toolkit/mcp/README.md b/toolkit/mcp/README.md index 4d4d123..b583c9c 100644 --- a/toolkit/mcp/README.md +++ b/toolkit/mcp/README.md @@ -8,8 +8,8 @@ Server MCP locale, read-only, per ispezionare rapidamente path risolti, schemi e - `toolkit_show_schema(config_path, layer="clean", year=0)` - `toolkit_run_summary(config_path, year=0)` — statistiche aggregate (totali, successi, durata media) - `toolkit_summary(config_path, year=0)` — dashboard diagnostico (layer + run + warnings) -- `toolkit_blocker_hints(config_path, year=0)` -- `toolkit_review_readiness(config_path, year=0)` +- `toolkit_blocker_hints(config_path, year=0)` — ⚠️ deprecato, usa `toolkit_review_readiness` +- `toolkit_review_readiness(config_path, year=0)` — (raccomandato) - `toolkit_list_runs(config_path, year=0, since=None, until=None, status=None, limit=20, cross_year=False)` - `toolkit_schema_diff(config_path)` — confronto segnali schema raw cross-year (encoding, colonne, ecc.) - `toolkit_csv_preview(csv_path, limit=20)` — schema + preview CSV via profiler pipeline (`sniff_source_file` + `profile_with_read_cfg`); output allineato con `RawProfile` (delim, encoding, decimal, skip, robust_read_suggested) @@ -55,5 +55,5 @@ Sostituire il path del `command` con il Python reale del clone locale che usera' - `toolkit_csv_preview` legge un CSV usando la stessa pipeline di `profile_raw` (`sniff_source_file` + `profile_with_read_cfg`); restituisce schema + prime N righe + mapping_suggestions — utile per ispezionare file raw senza runnare la pipeline - `toolkit_run_summary` aggrega tutti i run record per dataset/year - `toolkit_summary` include `run.latest_run_record` (payload completo dell'ultimo run) -- `toolkit_blocker_hints` evidenzia mismatch pratici tra output risolti e stato run +- `toolkit_blocker_hints` ⚠️ deprecato, punta a `toolkit_review_readiness` - `toolkit_review_readiness` esegue check di readiness per review candidate: config valida, layer presenti, output leggibili, coerenza run record From 1f8ca0983a10bbf6c60fa5de15c09d2cd47d2c40 Mon Sep 17 00:00:00 2001 From: Zio Gabber <78922322+Gabrymi93@users.noreply.github.com> Date: Fri, 15 May 2026 22:03:00 +0100 Subject: [PATCH 3/6] =?UTF-8?q?chore:=20nascosto=20blocker-hints=20da=20he?= =?UTF-8?q?lp=20(hidden=3DTrue)=20=E2=80=94=20deprecato?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- toolkit/cli/cmd_blocker_hints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolkit/cli/cmd_blocker_hints.py b/toolkit/cli/cmd_blocker_hints.py index 24048f9..2d7c4b8 100644 --- a/toolkit/cli/cmd_blocker_hints.py +++ b/toolkit/cli/cmd_blocker_hints.py @@ -96,4 +96,4 @@ def blocker_hints( def register(app: typer.Typer) -> None: - app.command("blocker-hints")(blocker_hints) + app.command("blocker-hints", hidden=True)(blocker_hints) From 1569d7adb2b20d9130242975a0daf611a986b21a Mon Sep 17 00:00:00 2001 From: Zio Gabber <78922322+Gabrymi93@users.noreply.github.com> Date: Fri, 15 May 2026 22:07:53 +0100 Subject: [PATCH 4/6] =?UTF-8?q?chore:=20descrizioni=20per=20run,=20inspect?= =?UTF-8?q?,=20scaffold=20=E2=80=94=20help=20completo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- toolkit/cli/cmd_run.py | 2 +- toolkit/cli/cmd_scaffold.py | 2 +- toolkit/cli/inspect/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/toolkit/cli/cmd_run.py b/toolkit/cli/cmd_run.py index 4bf585c..928a7c5 100644 --- a/toolkit/cli/cmd_run.py +++ b/toolkit/cli/cmd_run.py @@ -527,4 +527,4 @@ def register(app: typer.Typer) -> None: run_sub.command("cross-year")(run_cross_year_cmd) # alias hyphen run_sub.command("init")(run_init) run_sub.command("full")(run_full) - app.add_typer(run_sub, name="run") + app.add_typer(run_sub, name="run", help="Esegue la pipeline RAW → CLEAN → MART per un dataset.") diff --git a/toolkit/cli/cmd_scaffold.py b/toolkit/cli/cmd_scaffold.py index c5b1eaa..6f68c1f 100644 --- a/toolkit/cli/cmd_scaffold.py +++ b/toolkit/cli/cmd_scaffold.py @@ -113,4 +113,4 @@ def scaffold_clean( def register(app: typer.Typer) -> None: scaffold_app = typer.Typer(no_args_is_help=True, add_completion=False) scaffold_app.command("clean")(scaffold_clean) - app.add_typer(scaffold_app, name="scaffold") + app.add_typer(scaffold_app, name="scaffold", help="Genera scheletro candidate: dataset.yml, SQL template.") diff --git a/toolkit/cli/inspect/__init__.py b/toolkit/cli/inspect/__init__.py index 57e5230..71e70c5 100644 --- a/toolkit/cli/inspect/__init__.py +++ b/toolkit/cli/inspect/__init__.py @@ -19,4 +19,4 @@ def register(app: typer.Typer) -> None: inspect_app.command("schema")(schema) inspect_app.command("url")(url) inspect_app.command("probe")(probe) - app.add_typer(inspect_app, name="inspect") + app.add_typer(inspect_app, name="inspect", help="Ispeziona path, schema, readiness e URL del dataset.") From 7e908a01f737df02b92314acef6ab70e87eec860 Mon Sep 17 00:00:00 2001 From: Zio Gabber <78922322+Gabrymi93@users.noreply.github.com> Date: Fri, 15 May 2026 22:12:08 +0100 Subject: [PATCH 5/6] chore: rimuovo import sys inutilizzato --- toolkit/cli/cmd_blocker_hints.py | 1 - 1 file changed, 1 deletion(-) diff --git a/toolkit/cli/cmd_blocker_hints.py b/toolkit/cli/cmd_blocker_hints.py index 2d7c4b8..2fbf529 100644 --- a/toolkit/cli/cmd_blocker_hints.py +++ b/toolkit/cli/cmd_blocker_hints.py @@ -6,7 +6,6 @@ from __future__ import annotations -import sys from pathlib import Path from toolkit.mcp.schema_ops import blocker_hints as _blocker_hints From 82def331f0bc4749ddc2e6e7db688a7521b7a31a Mon Sep 17 00:00:00 2001 From: Zio Gabber <78922322+Gabrymi93@users.noreply.github.com> Date: Fri, 15 May 2026 22:22:10 +0100 Subject: [PATCH 6/6] fix: inspect schema mantiene compatibilita' col posizionale + -c --- toolkit/cli/inspect/schema_ops.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/toolkit/cli/inspect/schema_ops.py b/toolkit/cli/inspect/schema_ops.py index 3759adf..9c92438 100644 --- a/toolkit/cli/inspect/schema_ops.py +++ b/toolkit/cli/inspect/schema_ops.py @@ -10,7 +10,8 @@ def schema( - config: str = typer.Option(..., "--config", "-c", help="Path al dataset.yml"), + config_path: str = typer.Argument("", metavar="CONFIG", help="Path al dataset.yml (posizionale)"), + config: str = typer.Option(None, "--config", "-c", help="Path al dataset.yml", hidden=True), layer: str = typer.Option("clean", "--layer", "-l", help="Layer: raw, clean, mart"), year: int = typer.Option(0, "--year", "-y", help="Anno (default: ultimo)"), json_output: bool = typer.Option(False, "--json", help="Output JSON"), @@ -18,8 +19,17 @@ def schema( """Mostra lo schema (colonne + tipi) di raw, clean o mart. Chiama la stessa implementazione del tool MCP toolkit_show_schema. + + Il path config puo' essere passato come argomento posizionale + (es. toolkit inspect schema path/to/dataset.yml) + o con l'opzione --config / -c. """ - result = show_schema(config, layer, year or None) + resolved_config = config or config_path + if not resolved_config: + typer.echo("error: specificare il path al dataset.yml (argomento o --config)", err=True) + raise typer.Exit(code=1) + + result = show_schema(resolved_config, layer, year or None) status = result.get("status", "ok" if result.get("columns") else "empty") if json_output: