zendev-proposal 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ """Repository-native proposal validation and indexing."""
2
+
3
+ from zendev.proposal.config import load_config
4
+ from zendev.proposal.indexing import build_index, expected_index_text
5
+ from zendev.proposal.model import Diagnostic, ProposalConfig, ProposalDocument
6
+ from zendev.proposal.validation import validate_repository
7
+
8
+ __all__ = [
9
+ "Diagnostic",
10
+ "ProposalConfig",
11
+ "ProposalDocument",
12
+ "build_index",
13
+ "expected_index_text",
14
+ "load_config",
15
+ "validate_repository",
16
+ ]
@@ -0,0 +1,18 @@
1
+ """Markdown scanning used internally by proposal validation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator
6
+
7
+
8
+ def iter_lines_outside_fences(markdown: str) -> Iterator[str]:
9
+ """Yield raw lines outside fenced code blocks."""
10
+
11
+ in_fence = False
12
+ for line in markdown.splitlines():
13
+ stripped = line.strip()
14
+ if stripped.startswith("```"):
15
+ in_fence = not in_fence
16
+ continue
17
+ if not in_fence:
18
+ yield line
zendev/proposal/cli.py ADDED
@@ -0,0 +1,208 @@
1
+ """Command-line interface for repository-native proposal checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ from collections.abc import Sequence
8
+ from pathlib import Path
9
+ from typing import Annotated
10
+
11
+ import typer
12
+
13
+ from zendev.proposal.config import load_config
14
+ from zendev.proposal.indexing import check_index, write_index
15
+ from zendev.proposal.model import Diagnostic, ProposalToolError, RepositoryState
16
+ from zendev.proposal.validation import validate_repository
17
+
18
+ JSON_SCHEMA_VERSION = 1
19
+
20
+ app = typer.Typer(
21
+ add_completion=False,
22
+ help="Validate and index a repository-native proposal process.",
23
+ no_args_is_help=True,
24
+ pretty_exceptions_enable=False,
25
+ rich_markup_mode=None,
26
+ )
27
+
28
+
29
+ def _summary(state: RepositoryState, *, index_state: str) -> dict[str, object]:
30
+ return {
31
+ "formal_proposals": len(state.formal_documents),
32
+ "drafts": state.draft_count,
33
+ "index": index_state,
34
+ }
35
+
36
+
37
+ def _human_diagnostic(diagnostic: Diagnostic) -> str:
38
+ location = diagnostic.path or "proposal"
39
+ if diagnostic.line is not None:
40
+ location += f":{diagnostic.line}"
41
+ rendered = f"{location}: {diagnostic.code}: {diagnostic.message}"
42
+ if diagnostic.hint is not None:
43
+ rendered += f"\n hint: {diagnostic.hint}"
44
+ return rendered
45
+
46
+
47
+ def _emit(
48
+ *,
49
+ command: str,
50
+ diagnostics: Sequence[Diagnostic],
51
+ summary: dict[str, object] | None,
52
+ json_output: bool,
53
+ success_message: str | None = None,
54
+ ) -> None:
55
+ if json_output:
56
+ print(
57
+ json.dumps(
58
+ {
59
+ "schema_version": JSON_SCHEMA_VERSION,
60
+ "command": command,
61
+ "ok": not diagnostics,
62
+ "diagnostics": [diagnostic.as_dict() for diagnostic in diagnostics],
63
+ "summary": summary,
64
+ },
65
+ ensure_ascii=False,
66
+ sort_keys=True,
67
+ )
68
+ )
69
+ return
70
+ if diagnostics:
71
+ for diagnostic in diagnostics:
72
+ print(_human_diagnostic(diagnostic), file=sys.stderr)
73
+ return
74
+ if success_message is not None:
75
+ print(success_message)
76
+
77
+
78
+ def _emit_tool_error(*, command: str, error: ProposalToolError, json_output: bool) -> int:
79
+ _emit(
80
+ command=command,
81
+ diagnostics=[error.diagnostic],
82
+ summary=None,
83
+ json_output=json_output,
84
+ )
85
+ return 2
86
+
87
+
88
+ def _check(config_path: Path, base_ref: str | None, *, json_output: bool) -> int:
89
+ try:
90
+ config = load_config(config_path)
91
+ result = validate_repository(config, base_ref=base_ref)
92
+ diagnostics = list(result.diagnostics)
93
+ index_state = "not-checked"
94
+ if not diagnostics:
95
+ drift = check_index(config, result.state)
96
+ if drift is not None:
97
+ diagnostics.append(drift)
98
+ index_state = "drifted"
99
+ else:
100
+ index_state = "up-to-date"
101
+ except ProposalToolError as error:
102
+ return _emit_tool_error(command="check", error=error, json_output=json_output)
103
+
104
+ summary = _summary(result.state, index_state=index_state)
105
+ _emit(
106
+ command="check",
107
+ diagnostics=diagnostics,
108
+ summary=summary,
109
+ json_output=json_output,
110
+ success_message=(
111
+ f"Validated {summary['formal_proposals']} formal proposal(s), "
112
+ f"{summary['drafts']} draft(s), and the committed index."
113
+ ),
114
+ )
115
+ return 1 if diagnostics else 0
116
+
117
+
118
+ def _index(config_path: Path, *, write: bool, json_output: bool) -> int:
119
+ try:
120
+ config = load_config(config_path)
121
+ result = validate_repository(config)
122
+ diagnostics = list(result.diagnostics)
123
+ index_state = "not-checked"
124
+ changed = False
125
+ if not diagnostics and write:
126
+ changed = write_index(config, result.state)
127
+ index_state = "updated" if changed else "up-to-date"
128
+ elif not diagnostics:
129
+ drift = check_index(config, result.state)
130
+ if drift is not None:
131
+ diagnostics.append(drift)
132
+ index_state = "drifted"
133
+ else:
134
+ index_state = "up-to-date"
135
+ except ProposalToolError as error:
136
+ return _emit_tool_error(command="index", error=error, json_output=json_output)
137
+
138
+ summary = _summary(result.state, index_state=index_state)
139
+ _emit(
140
+ command="index",
141
+ diagnostics=diagnostics,
142
+ summary=summary,
143
+ json_output=json_output,
144
+ success_message=("Updated proposal index." if changed else "Proposal index is already up to date."),
145
+ )
146
+ return 1 if diagnostics else 0
147
+
148
+
149
+ @app.command("check")
150
+ def check_command(
151
+ config: Annotated[
152
+ Path,
153
+ typer.Option("--config", metavar="PATH", help="Proposal TOML policy."),
154
+ ] = Path("proposal.toml"),
155
+ base_ref: Annotated[
156
+ str | None,
157
+ typer.Option(
158
+ "--base-ref",
159
+ envvar="PROPOSAL_BASE_REF",
160
+ help="Exact local Git ref used to validate lifecycle history.",
161
+ ),
162
+ ] = None,
163
+ json_output: Annotated[
164
+ bool,
165
+ typer.Option("--json", help="Emit stable JSON diagnostics."),
166
+ ] = False,
167
+ ) -> None:
168
+ """Validate proposals, history, graph, and the committed index."""
169
+
170
+ exit_code = _check(config, base_ref, json_output=json_output)
171
+ if exit_code:
172
+ raise typer.Exit(code=exit_code)
173
+
174
+
175
+ @app.command("index")
176
+ def index_command(
177
+ config: Annotated[
178
+ Path,
179
+ typer.Option("--config", metavar="PATH", help="Proposal TOML policy."),
180
+ ] = Path("proposal.toml"),
181
+ check: Annotated[
182
+ bool,
183
+ typer.Option("--check", help="Fail when the index has drifted."),
184
+ ] = False,
185
+ write: Annotated[
186
+ bool,
187
+ typer.Option("--write", help="Write the deterministic index."),
188
+ ] = False,
189
+ json_output: Annotated[
190
+ bool,
191
+ typer.Option("--json", help="Emit stable JSON diagnostics."),
192
+ ] = False,
193
+ ) -> None:
194
+ """Check or explicitly write the deterministic proposal index."""
195
+
196
+ if check == write:
197
+ raise typer.BadParameter("exactly one of --check or --write is required", param_hint="--check/--write")
198
+ exit_code = _index(config, write=write, json_output=json_output)
199
+ if exit_code:
200
+ raise typer.Exit(code=exit_code)
201
+
202
+
203
+ def main() -> None:
204
+ app(prog_name="zendev-proposal")
205
+
206
+
207
+ if __name__ == "__main__":
208
+ raise SystemExit(main())