feat(s53): CLI mode-exclusivity refusals (portfolio vs single-project partition)
After parse_args, validate mode consistency with structured refusals (rc 1, not argparse.error): --portfolio + any single-project-only flag (--docs-dir/--bundle-dir/--verdict-dir/--outbox-dir/ --run-id/--live-dry-run) is refused naming the offending flag; --goals/--ledger outside --portfolio is refused. --decision/--rationale are EXCLUDED (non-None defaults make explicit-vs-default indistinguishable — Pass-2 #2; inert in portfolio mode, README says so). --dimension-config is valid in both modes. Validation precedes the portfolio dispatch, so refusals fire before any load. RED-first: (a)/(a')/(c) failed offline (rc 0 fall-through) before the check, green after; (b)/(b') single-project guard + legacy backward-compat pin were already green post-Step-3. All refusal-arm RED fall-throughs held OFFLINE (met-goal / --live-dry-run) — no socket, per brief NFR. 15 passed, ruff + mypy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KNNiJRk1sSwxgVLS5AobT1
This commit is contained in:
parent
905b2f9a43
commit
1b990f0887
2 changed files with 143 additions and 0 deletions
|
|
@ -677,6 +677,36 @@ def main(argv: list[str] | None = None) -> int:
|
|||
)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
# 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.
|
||||
# --decision/--rationale are excluded: their non-None argparse defaults make an explicit value
|
||||
# indistinguishable from the default, so an honest refusal is unimplementable (they are inert in
|
||||
# portfolio mode; the README documents that). --dimension-config is valid in BOTH modes.
|
||||
if args.portfolio:
|
||||
single_only = {
|
||||
"--docs-dir": args.docs_dir,
|
||||
"--bundle-dir": args.bundle_dir,
|
||||
"--verdict-dir": args.verdict_dir,
|
||||
"--outbox-dir": args.outbox_dir,
|
||||
"--run-id": args.run_id,
|
||||
"--live-dry-run": args.live_dry_run,
|
||||
}
|
||||
offending = [name for name, value in single_only.items() if value]
|
||||
if offending:
|
||||
print(
|
||||
f"portfolio run refused: {', '.join(offending)} belong to single-project mode, "
|
||||
"not --portfolio (the two CLI modes are a documented partition)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
elif args.goals is not None or args.ledger is not None:
|
||||
print(
|
||||
"run refused: --goals/--ledger require --portfolio mode",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
if args.portfolio:
|
||||
# Portfolio mode (Step 3): dispatch to the EXISTING run_portfolio via the fail-fast loaders
|
||||
# (run_portfolio itself is unchanged). Loader/ValueError failures surface through the same
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue