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.
- zendev/proposal/__init__.py +16 -0
- zendev/proposal/_markdown_scan.py +18 -0
- zendev/proposal/cli.py +208 -0
- zendev/proposal/config.py +670 -0
- zendev/proposal/indexing.py +140 -0
- zendev/proposal/model.py +171 -0
- zendev/proposal/py.typed +0 -0
- zendev/proposal/repository.py +192 -0
- zendev/proposal/validation.py +750 -0
- zendev_proposal-0.2.0.dist-info/METADATA +300 -0
- zendev_proposal-0.2.0.dist-info/RECORD +13 -0
- zendev_proposal-0.2.0.dist-info/WHEEL +4 -0
- zendev_proposal-0.2.0.dist-info/entry_points.txt +2 -0
|
@@ -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())
|