feat(harness): port CLI skeleton + nsp phase meta --json (F2, kills JS/Python drift)
nsp.cli app registers phase_app (the gate's workflow-fact source). phase meta
--json emits {schema_version, max_phase, phases:[{num,id,name,advance,artifacts_out}]}
from load_workflow() -- the single source of truth. F8/datamart is present (the
exact JS-drift bug in ChironeWp3, PHASE_NAMES truncated to 7, is structurally gone).
Scope: the 12 other command groups land in their porting tasks (A9 ports
db/mschema/_guards; B1 vectorstore; B3 search/lshindex). Eager-importing them now
would break the app on unported deps -- deferred to keep the suite green at each commit.
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
__version__ = "0.1.0"
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -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
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -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"
|
||||||
Reference in New Issue
Block a user