feat(s54): --report/--json CLI mode in run.py over value_report
This commit is contained in:
parent
9b81a6e416
commit
19000d89f6
2 changed files with 193 additions and 0 deletions
|
|
@ -59,6 +59,11 @@ from portfolio_optimiser.verdicts import (
|
|||
capture_verdict,
|
||||
load_verdicts_from_dir,
|
||||
)
|
||||
from portfolio_optimiser.value_report import (
|
||||
build_value_report,
|
||||
dump_report_json,
|
||||
format_report_text,
|
||||
)
|
||||
from portfolio_optimiser.workflow import _MAKER_CHECKER_ROLES, fresh_workflow
|
||||
|
||||
|
||||
|
|
@ -675,8 +680,72 @@ def main(argv: list[str] | None = None) -> int:
|
|||
action="store_true",
|
||||
help="offline drill: build contracts/clients/budget, STOP before the first model call",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--report",
|
||||
action="store_true",
|
||||
help="S5.4 read-only value report: roll up the --ledger's realized savings (per-project + "
|
||||
"portfolio totals, flagged cross-dimension overlaps, per-entry provenance) to stdout. "
|
||||
"Mode-exclusive: only --ledger/--json are permitted alongside it; makes NO model calls",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="value report output form (requires --report): emit the roll-up as deterministic JSON "
|
||||
"instead of the human table",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
# S5.4: read-only value-report dispatch — placed FIRST (right after parse_args, BEFORE the
|
||||
# mode-exclusivity block below) so it returns before any model/portfolio path can start and no
|
||||
# later branch can shadow it (the bare `--ledger`-outside-portfolio refusal at the elif below is
|
||||
# left UNCHANGED — a bare --ledger with no --report still flows there and refuses as before).
|
||||
if args.json and not args.report:
|
||||
# A stray --json is never silently ignored (honors S5.3's "refused, never ignored" partition).
|
||||
print("run refused: --json requires --report", file=sys.stderr)
|
||||
return 1
|
||||
if args.report:
|
||||
# Mode-exclusivity as an ALLOWLIST (not a short blocklist): report mode permits ONLY --ledger
|
||||
# and --json; ANY other distinguishable mode/config flag is refused — else --report --goals
|
||||
# would silently drop --goals, whereas bare --goals is refused below (adding --report must not
|
||||
# suppress an existing refusal). --decision/--rationale are excluded: their non-None argparse
|
||||
# defaults are indistinguishable from an explicit value (exactly as the block below excludes
|
||||
# them); they are inert in report mode.
|
||||
report_forbidden = {
|
||||
"--portfolio": args.portfolio,
|
||||
"--live-dry-run": args.live_dry_run,
|
||||
"PROJECT_ID": args.project_id is not None,
|
||||
"--goals": args.goals is not None,
|
||||
"--docs-dir": args.docs_dir is not None,
|
||||
"--bundle-dir": args.bundle_dir is not None,
|
||||
"--verdict-dir": args.verdict_dir is not None,
|
||||
"--outbox-dir": args.outbox_dir is not None,
|
||||
"--run-id": args.run_id is not None,
|
||||
"--dimension-config": args.dimension_config is not None,
|
||||
}
|
||||
if any(report_forbidden.values()):
|
||||
print(
|
||||
"run report refused: mode-exclusive (only --ledger/--json permitted with --report)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
if not args.ledger:
|
||||
# Guards SavingsLedger.load(None) -> Path(None) TypeError (NOT in the load except tuple).
|
||||
print("run report refused: --report requires --ledger <file>", file=sys.stderr)
|
||||
return 1
|
||||
try:
|
||||
# `report_ledger`, not `ledger`: the portfolio branch below binds `ledger` as
|
||||
# `SavingsLedger | None`, so reusing that name here (type `SavingsLedger`) collides on
|
||||
# mypy's function-scoped declared type.
|
||||
report_ledger = SavingsLedger.load(args.ledger)
|
||||
except (FileNotFoundError, ValidationError, ValueError) as exc:
|
||||
# A load failure must never masquerade as a real zero-savings result (SC5): stderr + rc 1,
|
||||
# no table. Only a successfully-loaded (possibly empty) ledger prints.
|
||||
print(f"run report refused: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
rep = build_value_report(report_ledger) # NB: `rep`, not `report` (`report` is bound below)
|
||||
print(dump_report_json(rep) if args.json else format_report_text(rep))
|
||||
return 0
|
||||
|
||||
# Step 4: mode-exclusivity validation (structured refusal, NOT argparse.error — keeps the rc 1
|
||||
# refusal contract). The two CLI modes are a documented partition: single-project-only flags are
|
||||
# refused in portfolio mode, and --goals/--ledger are refused outside it — never silently ignored.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue