diff --git a/harness/nsp/__init__.py b/harness/nsp/__init__.py index e69de29b..3dc1f76b 100644 --- a/harness/nsp/__init__.py +++ b/harness/nsp/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/harness/nsp/cli/__init__.py b/harness/nsp/cli/__init__.py new file mode 100644 index 00000000..4bea3ecf --- /dev/null +++ b/harness/nsp/cli/__init__.py @@ -0,0 +1,41 @@ +"""nsp CLI -- Typer app `nsp`. + +Minimal skeleton for now: registers `phase_app` (the gate needs `nsp phase meta --json`). +Other command groups (db, schema, lsh, vector, memory, session, decision, sql, cte, +evidence, search) are wired in their porting tasks, once their backend deps land +(A9 ports db/mschema/_guards; B1 ports vectorstore; B3 ports search/lshindex/sampling). +""" +from __future__ import annotations + +import typer + +from nsp import __version__ + +app = typer.Typer( + help="nsp (Natural Semantic Platform) -- kit operativo per generazione SQL assistita da Pi", + no_args_is_help=True, +) + + +def _version_callback(value: bool) -> None: + if value: + typer.echo(__version__) + raise typer.Exit() + + +@app.callback() +def main( + version: bool = typer.Option( + False, + "--version", + callback=_version_callback, + is_eager=True, + help="Mostra la versione ed esce.", + ), +) -> None: + pass + + +from nsp.cli.phase_cmd import phase_app # noqa: E402 + +app.add_typer(phase_app, name="phase") diff --git a/harness/nsp/cli/phase_cmd.py b/harness/nsp/cli/phase_cmd.py new file mode 100644 index 00000000..54edd120 --- /dev/null +++ b/harness/nsp/cli/phase_cmd.py @@ -0,0 +1,51 @@ +"""nsp phase -- workflow HITL gate commands. + +`meta` is the single source the gate (nsp-gate.js) reads workflow facts from, +killing the JS/Python PHASE_NAMES drift (F2). advance/reopen/show land with the +session/db ports (A9) -- they need current_phase + load_session_or_exit which +depend on the not-yet-ported session store helpers. +""" +from __future__ import annotations + +import json + +import typer + +from nsp.workflow import load_workflow + +phase_app = typer.Typer(help="Fase del workflow HITL (gate di avanzamento/ritorno)") + + +@phase_app.command("meta") +def meta_cmd( + as_json: bool = typer.Option( + False, + "--json", + help="Emette i metadati del workflow come JSON (usato dall'estensione gate).", + ), +) -> None: + """Stampa i metadati del workflow derivati da workflow.yaml (F2).""" + wf = load_workflow() + if not as_json: + typer.echo(f"max_phase: {wf.max_phase}") + for p in wf.phases: + typer.echo(f" {p.id} ({p.num}/{wf.max_phase}) {p.name} advance={p.advance}") + return + typer.echo( + json.dumps( + { + "schema_version": wf.schema_version, + "max_phase": wf.max_phase, + "phases": [ + { + "num": p.num, + "id": p.id, + "name": p.name, + "advance": p.advance, + "artifacts_out": p.artifacts_out, + } + for p in wf.phases + ], + } + ) + ) diff --git a/harness/tests/test_cli_phase_meta.py b/harness/tests/test_cli_phase_meta.py new file mode 100644 index 00000000..3e2a051d --- /dev/null +++ b/harness/tests/test_cli_phase_meta.py @@ -0,0 +1,32 @@ +"""L1: nsp phase meta --json -- the gate reads workflow facts from here (F2). +Kills the JS/Python drift (no mirrored constants): workflow.yaml is the single source. +""" +import json + +from typer.testing import CliRunner + +from nsp.cli import app + +runner = CliRunner() + + +def test_phase_meta_json_returns_workflow_data(): + result = runner.invoke(app, ["phase", "meta", "--json"]) + assert result.exit_code == 0 + data = json.loads(result.stdout) + assert data["max_phase"] == 8 + assert len(data["phases"]) == 8 + # F8 must be present — this is the exact JS-drift bug (PHASE_NAMES truncated to 7). + assert data["phases"][7]["name"] == "datamart" + assert data["phases"][7]["num"] == 8 + assert "advance" in data["phases"][0] + assert "artifacts_out" in data["phases"][0] + + +def test_phase_meta_each_phase_carries_id_and_name(): + result = runner.invoke(app, ["phase", "meta", "--json"]) + data = json.loads(result.stdout) + for i, p in enumerate(data["phases"], start=1): + assert p["num"] == i + assert p["id"], f"phase {i} missing id" + assert p["name"], f"phase {i} missing name"