next.py python
182 lines 5.2 KB
Raw
sha256:8d7f41aafae41deee70035a92f93602ef1722a290153597451e5932af503a42c docs: queue board-identity follow-ups so they survive the session Human 59 minutes ago
1 """``ok next`` — print the paste-ready NEXT fence from disk (§ONS.5 / §NXP)."""
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 REASON_REPO_ROOT_UNRESOLVED,
16 CurrentNextError,
17 CurrentNextResult,
18 absolute_repo_root,
19 extract_current_next,
20 format_current_next,
21 read_at_now,
22 )
23
24 EXIT_NEXT_MALFORMED = 37
25 EXIT_CONFIG = 2
26
27
28 def _reject_lane_name(lane: str | None) -> str | None:
29 """Return an error detail when ``lane`` is not a safe identifier (§ONS.11)."""
30 if lane is None:
31 return None
32 if any(token in lane for token in ("/", "\\")) or ".." in lane:
33 return f"invalid lane name {lane!r} (identifiers only; no path segments)"
34 return None
35
36
37 def _resolved_lane_label(config, lane_arg: str | None) -> str | None:
38 """JSON ``lane``: null when docs.lanes unset; else resolved name (§ONS.5.5)."""
39 if config.docs.lanes is None:
40 return None
41 return (lane_arg or config.docs.default_lane or "").strip() or None
42
43
44 def _repo_name_or_none(config) -> str | None:
45 name = (config.repo.name or "").strip()
46 return name or None
47
48
49 def run_next_command(args: Namespace, ctx: CliContext) -> int:
50 """Print the CURRENT NEXT paste fence (read-only; no Muse/git)."""
51 repo_root = resolve_repo_root(cwd=ctx.cwd, repo_arg=args.repo, command="next")
52 config_path = resolve_config_path(repo_root, args.config)
53 if not is_within_repo(repo_root, config_path):
54 ctx.output.error("refused: config path outside repo root")
55 return 4
56
57 lane_arg = getattr(args, "lane", None)
58 bad_lane = _reject_lane_name(lane_arg)
59 if bad_lane is not None:
60 ctx.output.error(f"next: config — {bad_lane}")
61 return EXIT_CONFIG
62
63 repo_root_abs = absolute_repo_root(repo_root)
64 if repo_root_abs is None:
65 err = CurrentNextError(
66 reason=REASON_REPO_ROOT_UNRESOLVED,
67 detail="cannot resolve repo root for provenance",
68 )
69 return _emit_error(
70 ctx,
71 err,
72 repo_name=None,
73 repo_root=None,
74 read_at=read_at_now(),
75 exit_code=EXIT_CONFIG,
76 )
77
78 try:
79 config = load_config(config_path)
80 lane_docs = resolve_lane_docs(config, lane_arg)
81 except ConfigError as exc:
82 ctx.output.error(format_config_error(exc, repo_root))
83 return EXIT_CONFIG
84
85 rel_path = join_docs_rel(config.repo.root_relative_docs, lane_docs.handover)
86 handover_path = living_doc_abs(repo_root, config, lane_docs.handover)
87 if not is_within_repo(repo_root, handover_path):
88 ctx.output.error("refused: handover path outside repo root")
89 return 4
90
91 lane_label = _resolved_lane_label(config, lane_arg)
92 repo_name = _repo_name_or_none(config)
93 # Capture read_at at disk-read time (§NXP.3.2).
94 read_at = read_at_now()
95 outcome = extract_current_next(
96 handover_path,
97 repo_relative_path=rel_path,
98 lane=lane_label,
99 )
100
101 if isinstance(outcome, CurrentNextError):
102 return _emit_error(
103 ctx,
104 outcome,
105 repo_name=repo_name,
106 repo_root=repo_root_abs,
107 read_at=read_at,
108 )
109
110 return _emit_success(
111 ctx,
112 outcome,
113 repo_name=repo_name,
114 repo_root=repo_root_abs,
115 read_at=read_at,
116 )
117
118
119 def _emit_success(
120 ctx: CliContext,
121 result: CurrentNextResult,
122 *,
123 repo_name: str | None,
124 repo_root: str,
125 read_at: str,
126 ) -> int:
127 if ctx.output.json_mode:
128 ctx.output.emit_json(
129 {
130 "ok": True,
131 "path": result.path,
132 "lane": result.lane,
133 "heading": CURRENT_NEXT_HEADING,
134 "fence": result.fence,
135 "error": None,
136 "repo_name": repo_name,
137 "repo_root": repo_root,
138 "read_at": read_at,
139 }
140 )
141 return 0
142
143 # Product block — print even under --quiet (§ONS.5.4 / §NXP.3.5).
144 print(
145 format_current_next(
146 result,
147 repo_name=repo_name,
148 repo_root_abs=repo_root,
149 read_at=read_at,
150 ),
151 end="",
152 )
153 return 0
154
155
156 def _emit_error(
157 ctx: CliContext,
158 err: CurrentNextError,
159 *,
160 repo_name: str | None,
161 repo_root: str | None,
162 read_at: str,
163 exit_code: int = EXIT_NEXT_MALFORMED,
164 ) -> int:
165 if ctx.output.json_mode:
166 ctx.output.emit_json(
167 {
168 "ok": False,
169 "path": err.path,
170 "lane": err.lane,
171 "heading": CURRENT_NEXT_HEADING,
172 "fence": None,
173 "error": err.reason,
174 "message": err.message,
175 "repo_name": repo_name,
176 "repo_root": repo_root,
177 "read_at": read_at,
178 }
179 )
180 else:
181 ctx.output.error(err.message)
182 return exit_code
File History 1 commit
sha256:8d7f41aafae41deee70035a92f93602ef1722a290153597451e5932af503a42c docs: queue board-identity follow-ups so they survive the session Human 59 minutes ago