next.py python
114 lines 3.6 KB
Raw
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 6 days ago
1 """``ok next`` — print the paste-ready NEXT fence from disk (§ONS.5)."""
2
3 from __future__ import annotations
4
5 from argparse import Namespace
6
7 from adapters.config import load_config, resolve_lane_docs
8 from adapters.errors import ConfigError
9 from cli.context import CliContext
10 from cli.docs_paths import join_docs_rel, living_doc_abs
11 from cli.paths import is_within_repo, resolve_config_path, resolve_repo_root
12 from cli.sanitize import format_config_error
13 from tools.print_next.extract import (
14 CURRENT_NEXT_HEADING,
15 CurrentNextError,
16 CurrentNextResult,
17 extract_current_next,
18 format_current_next,
19 )
20
21 EXIT_NEXT_MALFORMED = 37
22
23
24 def _reject_lane_name(lane: str | None) -> str | None:
25 """Return an error detail when ``lane`` is not a safe identifier (§ONS.11)."""
26 if lane is None:
27 return None
28 if any(token in lane for token in ("/", "\\")) or ".." in lane:
29 return f"invalid lane name {lane!r} (identifiers only; no path segments)"
30 return None
31
32
33 def _resolved_lane_label(config, lane_arg: str | None) -> str | None:
34 """JSON ``lane``: null when docs.lanes unset; else resolved name (§ONS.5.5)."""
35 if config.docs.lanes is None:
36 return None
37 return (lane_arg or config.docs.default_lane or "").strip() or None
38
39
40 def run_next_command(args: Namespace, ctx: CliContext) -> int:
41 """Print the CURRENT NEXT paste fence (read-only; no Muse/git)."""
42 repo_root = resolve_repo_root(cwd=ctx.cwd, repo_arg=args.repo, command="next")
43 config_path = resolve_config_path(repo_root, args.config)
44 if not is_within_repo(repo_root, config_path):
45 ctx.output.error("refused: config path outside repo root")
46 return 4
47
48 lane_arg = getattr(args, "lane", None)
49 bad_lane = _reject_lane_name(lane_arg)
50 if bad_lane is not None:
51 ctx.output.error(f"next: config — {bad_lane}")
52 return 2
53
54 try:
55 config = load_config(config_path)
56 lane_docs = resolve_lane_docs(config, lane_arg)
57 except ConfigError as exc:
58 ctx.output.error(format_config_error(exc, repo_root))
59 return 2
60
61 rel_path = join_docs_rel(config.repo.root_relative_docs, lane_docs.handover)
62 handover_path = living_doc_abs(repo_root, config, lane_docs.handover)
63 if not is_within_repo(repo_root, handover_path):
64 ctx.output.error("refused: handover path outside repo root")
65 return 4
66
67 lane_label = _resolved_lane_label(config, lane_arg)
68 outcome = extract_current_next(
69 handover_path,
70 repo_relative_path=rel_path,
71 lane=lane_label,
72 )
73
74 if isinstance(outcome, CurrentNextError):
75 return _emit_error(ctx, outcome)
76
77 return _emit_success(ctx, outcome)
78
79
80 def _emit_success(ctx: CliContext, result: CurrentNextResult) -> int:
81 if ctx.output.json_mode:
82 ctx.output.emit_json(
83 {
84 "ok": True,
85 "path": result.path,
86 "lane": result.lane,
87 "heading": CURRENT_NEXT_HEADING,
88 "fence": result.fence,
89 "error": None,
90 }
91 )
92 return 0
93
94 # Product block — print even under --quiet (§ONS.5.4).
95 print(format_current_next(result), end="")
96 return 0
97
98
99 def _emit_error(ctx: CliContext, err: CurrentNextError) -> int:
100 if ctx.output.json_mode:
101 ctx.output.emit_json(
102 {
103 "ok": False,
104 "path": err.path,
105 "lane": err.lane,
106 "heading": CURRENT_NEXT_HEADING,
107 "fence": None,
108 "error": err.reason,
109 "message": err.message,
110 }
111 )
112 else:
113 ctx.output.error(err.message)
114 return EXIT_NEXT_MALFORMED
File History 1 commit
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 6 days ago