its-magic 0.1.2-43 → 0.1.2-48
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.
- package/README.md +119 -0
- package/package.json +1 -1
- package/scripts/check_intake_template_parity.py +67 -2
- package/scripts/guard_installer_publish.py +19 -1
- package/template/.cursor/commands/architecture.md +16 -6
- package/template/.cursor/commands/auto.md +42 -0
- package/template/.cursor/commands/qa.md +8 -0
- package/template/.cursor/commands/release.md +11 -0
- package/template/.cursor/commands/verify-work.md +16 -1
- package/template/.cursor/rules/caveman.mdc +43 -0
- package/template/.cursor/scratchpad.local.example.md +18 -4
- package/template/.cursor/scratchpad.md +14 -4
- package/template/.github/workflows/ci.yml +4 -114
- package/template/README.md +113 -0
- package/template/docs/engineering/auto-orchestration-reference.md +44 -1
- package/template/docs/engineering/context/installer-owned-paths.manifest +12 -0
- package/template/docs/engineering/context/readme-section-affinity.json +30 -0
- package/template/docs/engineering/runbook.md +118 -5
- package/template/scripts/auto_outer_driver.py +521 -0
- package/template/scripts/check_downstream_ci_guard.py +67 -0
- package/template/scripts/check_intake_template_parity.py +67 -2
- package/template/scripts/downstream_ci_guard_lib.py +222 -0
- package/template/scripts/enforce-triad-hot-surface.py +135 -8
- package/template/scripts/readme_feature_coverage_lib.py +608 -0
- package/template/scripts/uat_probe_lib.py +317 -0
- package/template/scripts/validate_readme_feature_coverage.py +140 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
UAT probe resolver (US-0092 / DEC-0078).
|
|
4
|
+
|
|
5
|
+
Shared by /verify-work and /qa for self-verify acceptance steps.
|
|
6
|
+
Fail-closed — no silent PASS.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import urllib.error
|
|
16
|
+
import urllib.request
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
UAT_PROBE_UNRESOLVED = "UAT_PROBE_UNRESOLVED"
|
|
20
|
+
UAT_STACK_PROFILE_UNKNOWN = "UAT_STACK_PROFILE_UNKNOWN"
|
|
21
|
+
UAT_PROBE_TIMEOUT = "UAT_PROBE_TIMEOUT"
|
|
22
|
+
UAT_PROBE_FAILED = "UAT_PROBE_FAILED"
|
|
23
|
+
UAT_PROBE_FORBIDDEN = "UAT_PROBE_FORBIDDEN"
|
|
24
|
+
UAT_PROBE_PASS = "UAT_PROBE_PASS"
|
|
25
|
+
|
|
26
|
+
PROBE_KINDS = (
|
|
27
|
+
"build",
|
|
28
|
+
"test",
|
|
29
|
+
"api_health",
|
|
30
|
+
"process_health",
|
|
31
|
+
"browser_smoke",
|
|
32
|
+
"cli_smoke",
|
|
33
|
+
"manual_operator",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
FORBIDDEN_PATH_TOKENS = (".env", "intake_evidence", "handoffs/intake_evidence")
|
|
37
|
+
|
|
38
|
+
DEFAULT_PROBE_TIMEOUT = 120
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _merge_scratchpad(repo: Path) -> dict[str, str]:
|
|
42
|
+
values: dict[str, str] = {}
|
|
43
|
+
for name in (".cursor/scratchpad.md", ".cursor/scratchpad.local.md"):
|
|
44
|
+
path = repo / name
|
|
45
|
+
if not path.is_file():
|
|
46
|
+
continue
|
|
47
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
48
|
+
stripped = line.strip()
|
|
49
|
+
if not stripped or stripped.startswith("#"):
|
|
50
|
+
continue
|
|
51
|
+
if "=" in stripped:
|
|
52
|
+
key, _, val = stripped.partition("=")
|
|
53
|
+
values[key.strip()] = val.strip()
|
|
54
|
+
return values
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def detect_stack_profile(repo: Path) -> str | None:
|
|
58
|
+
if (repo / "package.json").is_file():
|
|
59
|
+
return "node"
|
|
60
|
+
if (repo / "pyproject.toml").is_file() or (repo / "setup.py").is_file():
|
|
61
|
+
return "python"
|
|
62
|
+
if (repo / "go.mod").is_file():
|
|
63
|
+
return "go"
|
|
64
|
+
if list(repo.glob("*.csproj")):
|
|
65
|
+
return "dotnet"
|
|
66
|
+
if (repo / "pom.xml").is_file():
|
|
67
|
+
return "java"
|
|
68
|
+
readme = repo / "README.md"
|
|
69
|
+
if readme.is_file() and "generated" in readme.read_text(encoding="utf-8").lower():
|
|
70
|
+
return "generated"
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _forbidden(step_text: str) -> bool:
|
|
75
|
+
lower = step_text.lower()
|
|
76
|
+
return any(tok in lower for tok in FORBIDDEN_PATH_TOKENS)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _read_test_command(repo: Path) -> str | None:
|
|
80
|
+
runbook = repo / "docs" / "engineering" / "runbook.md"
|
|
81
|
+
if runbook.is_file():
|
|
82
|
+
m = re.search(r"^TEST_COMMAND:\s*(.+)$", runbook.read_text(encoding="utf-8"), re.M)
|
|
83
|
+
if m:
|
|
84
|
+
cmd = m.group(1).strip()
|
|
85
|
+
if cmd and "not configured" not in cmd.lower():
|
|
86
|
+
return cmd
|
|
87
|
+
merged = _merge_scratchpad(repo)
|
|
88
|
+
return merged.get("TEST_COMMAND") or None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _read_build_command(repo: Path, profile: str | None) -> str | None:
|
|
92
|
+
if profile == "node" and (repo / "package.json").is_file():
|
|
93
|
+
try:
|
|
94
|
+
pkg = json.loads((repo / "package.json").read_text(encoding="utf-8"))
|
|
95
|
+
scripts = pkg.get("scripts", {})
|
|
96
|
+
if "build" in scripts:
|
|
97
|
+
return "npm run build"
|
|
98
|
+
except (json.JSONDecodeError, OSError):
|
|
99
|
+
pass
|
|
100
|
+
if profile == "python" and (repo / "pyproject.toml").is_file():
|
|
101
|
+
return "python -m build"
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _read_health_url(repo: Path) -> str | None:
|
|
106
|
+
rc = repo / "docs" / "engineering" / "runtime-connectivity.md"
|
|
107
|
+
if rc.is_file():
|
|
108
|
+
m = re.search(r"https?://[^\s\)]+", rc.read_text(encoding="utf-8"))
|
|
109
|
+
if m:
|
|
110
|
+
return m.group(0).rstrip(".,)")
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def classify_step(step_text: str, repo: Path) -> tuple[str | None, str]:
|
|
115
|
+
if _forbidden(step_text):
|
|
116
|
+
return None, UAT_PROBE_FORBIDDEN
|
|
117
|
+
lower = step_text.lower()
|
|
118
|
+
profile = detect_stack_profile(repo)
|
|
119
|
+
|
|
120
|
+
if any(w in lower for w in ("manual", "operator", "human", "judgment", "visually")):
|
|
121
|
+
return "manual_operator", UAT_PROBE_UNRESOLVED
|
|
122
|
+
|
|
123
|
+
if any(w in lower for w in ("build", "compile", "bundle")):
|
|
124
|
+
if _read_build_command(repo, profile):
|
|
125
|
+
return "build", ""
|
|
126
|
+
return None, UAT_PROBE_UNRESOLVED
|
|
127
|
+
|
|
128
|
+
if any(w in lower for w in ("test", "pytest", "unit test", "integration test")):
|
|
129
|
+
if _read_test_command(repo) or profile in ("python", "node", "go", "generated"):
|
|
130
|
+
return "test", ""
|
|
131
|
+
return None, UAT_PROBE_UNRESOLVED
|
|
132
|
+
|
|
133
|
+
if any(w in lower for w in ("api", "health", "endpoint", "http", "rest")):
|
|
134
|
+
if _read_health_url(repo):
|
|
135
|
+
return "api_health", ""
|
|
136
|
+
return None, UAT_PROBE_UNRESOLVED
|
|
137
|
+
|
|
138
|
+
if any(w in lower for w in ("process", "startup", "server start", "readiness")):
|
|
139
|
+
return "process_health", UAT_PROBE_UNRESOLVED
|
|
140
|
+
|
|
141
|
+
if any(w in lower for w in ("browser", "playwright", "smoke", "ui")):
|
|
142
|
+
if profile == "node" or _read_health_url(repo):
|
|
143
|
+
return "browser_smoke", ""
|
|
144
|
+
return None, UAT_PROBE_UNRESOLVED
|
|
145
|
+
|
|
146
|
+
if any(w in lower for w in ("cli", "command line", "exit code")):
|
|
147
|
+
return "cli_smoke", UAT_PROBE_UNRESOLVED
|
|
148
|
+
|
|
149
|
+
if profile is None:
|
|
150
|
+
return None, UAT_STACK_PROFILE_UNKNOWN
|
|
151
|
+
return None, UAT_PROBE_UNRESOLVED
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def execute_probe(
|
|
155
|
+
kind: str,
|
|
156
|
+
step_text: str,
|
|
157
|
+
repo: Path,
|
|
158
|
+
*,
|
|
159
|
+
timeout: int = DEFAULT_PROBE_TIMEOUT,
|
|
160
|
+
) -> dict[str, object]:
|
|
161
|
+
profile = detect_stack_profile(repo)
|
|
162
|
+
result: dict[str, object] = {
|
|
163
|
+
"probe_kind": kind,
|
|
164
|
+
"step": step_text[:200],
|
|
165
|
+
"stack_profile": profile or "unknown",
|
|
166
|
+
"reason_code": UAT_PROBE_UNRESOLVED,
|
|
167
|
+
"passed": False,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if kind == "manual_operator":
|
|
171
|
+
result["reason_code"] = UAT_PROBE_UNRESOLVED
|
|
172
|
+
return result
|
|
173
|
+
|
|
174
|
+
if kind == "build":
|
|
175
|
+
cmd = _read_build_command(repo, profile)
|
|
176
|
+
if not cmd:
|
|
177
|
+
result["reason_code"] = UAT_PROBE_UNRESOLVED
|
|
178
|
+
return result
|
|
179
|
+
return _run_subprocess(cmd, repo, timeout, result)
|
|
180
|
+
|
|
181
|
+
if kind == "test":
|
|
182
|
+
cmd = _read_test_command(repo)
|
|
183
|
+
if not cmd:
|
|
184
|
+
if profile == "python":
|
|
185
|
+
cmd = "python -m pytest -q"
|
|
186
|
+
elif profile == "node":
|
|
187
|
+
cmd = "npm test"
|
|
188
|
+
elif profile == "generated":
|
|
189
|
+
cmd = "python -m pytest -q"
|
|
190
|
+
else:
|
|
191
|
+
result["reason_code"] = UAT_PROBE_UNRESOLVED
|
|
192
|
+
return result
|
|
193
|
+
return _run_subprocess(cmd, repo, timeout, result)
|
|
194
|
+
|
|
195
|
+
if kind == "api_health":
|
|
196
|
+
url = _read_health_url(repo)
|
|
197
|
+
if not url:
|
|
198
|
+
result["reason_code"] = UAT_PROBE_UNRESOLVED
|
|
199
|
+
return result
|
|
200
|
+
try:
|
|
201
|
+
req = urllib.request.Request(url, method="GET")
|
|
202
|
+
with urllib.request.urlopen(req, timeout=min(timeout, 30)) as resp:
|
|
203
|
+
result["status_code"] = resp.status
|
|
204
|
+
result["reason_code"] = UAT_PROBE_PASS
|
|
205
|
+
result["passed"] = 200 <= resp.status < 400
|
|
206
|
+
if not result["passed"]:
|
|
207
|
+
result["reason_code"] = UAT_PROBE_FAILED
|
|
208
|
+
except urllib.error.URLError as exc:
|
|
209
|
+
result["reason_code"] = UAT_PROBE_FAILED
|
|
210
|
+
result["error"] = type(exc).__name__
|
|
211
|
+
except TimeoutError:
|
|
212
|
+
result["reason_code"] = UAT_PROBE_TIMEOUT
|
|
213
|
+
return result
|
|
214
|
+
|
|
215
|
+
if kind in ("process_health", "browser_smoke", "cli_smoke"):
|
|
216
|
+
result["reason_code"] = UAT_PROBE_UNRESOLVED
|
|
217
|
+
return result
|
|
218
|
+
|
|
219
|
+
result["reason_code"] = UAT_PROBE_UNRESOLVED
|
|
220
|
+
return result
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _run_subprocess(
|
|
224
|
+
cmd: str,
|
|
225
|
+
repo: Path,
|
|
226
|
+
timeout: int,
|
|
227
|
+
result: dict[str, object],
|
|
228
|
+
) -> dict[str, object]:
|
|
229
|
+
result["command"] = cmd
|
|
230
|
+
try:
|
|
231
|
+
proc = subprocess.run(
|
|
232
|
+
cmd,
|
|
233
|
+
shell=True,
|
|
234
|
+
cwd=str(repo),
|
|
235
|
+
timeout=timeout,
|
|
236
|
+
capture_output=True,
|
|
237
|
+
text=True,
|
|
238
|
+
)
|
|
239
|
+
result["exit_code"] = proc.returncode
|
|
240
|
+
if proc.returncode == 0:
|
|
241
|
+
result["reason_code"] = UAT_PROBE_PASS
|
|
242
|
+
result["passed"] = True
|
|
243
|
+
else:
|
|
244
|
+
result["reason_code"] = UAT_PROBE_FAILED
|
|
245
|
+
except subprocess.TimeoutExpired:
|
|
246
|
+
result["reason_code"] = UAT_PROBE_TIMEOUT
|
|
247
|
+
except OSError as exc:
|
|
248
|
+
result["reason_code"] = UAT_PROBE_FAILED
|
|
249
|
+
result["error"] = str(exc)
|
|
250
|
+
return result
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def resolve_and_probe(step_text: str, repo: Path) -> dict[str, object]:
|
|
254
|
+
kind, pre_reason = classify_step(step_text, Path(repo))
|
|
255
|
+
if kind is None:
|
|
256
|
+
return {
|
|
257
|
+
"probe_kind": None,
|
|
258
|
+
"reason_code": pre_reason or UAT_PROBE_UNRESOLVED,
|
|
259
|
+
"passed": False,
|
|
260
|
+
}
|
|
261
|
+
if pre_reason == UAT_PROBE_FORBIDDEN:
|
|
262
|
+
return {"probe_kind": kind, "reason_code": UAT_PROBE_FORBIDDEN, "passed": False}
|
|
263
|
+
return execute_probe(kind, step_text, Path(repo))
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def probe_steps(steps: list[str], repo: Path) -> list[dict[str, object]]:
|
|
267
|
+
return [resolve_and_probe(step, repo) for step in steps]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def self_test() -> None:
|
|
271
|
+
repo = Path(__file__).resolve().parents[1]
|
|
272
|
+
assert UAT_PROBE_PASS in PROBE_KINDS or UAT_PROBE_PASS == "UAT_PROBE_PASS"
|
|
273
|
+
assert "build" in PROBE_KINDS
|
|
274
|
+
r = classify_step("run unit tests", repo)
|
|
275
|
+
assert r[0] == "test" or r[1] in (UAT_PROBE_UNRESOLVED, "")
|
|
276
|
+
r2 = classify_step("read secrets from .env file", repo)
|
|
277
|
+
assert r2[1] == UAT_PROBE_FORBIDDEN
|
|
278
|
+
r3 = classify_step("operator manually verifies UI", repo)
|
|
279
|
+
assert r3[0] == "manual_operator"
|
|
280
|
+
assert detect_stack_profile(repo) in ("python", "node", None, "generated")
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def main() -> int:
|
|
284
|
+
import argparse
|
|
285
|
+
|
|
286
|
+
parser = argparse.ArgumentParser(description="UAT probe resolver (US-0092).")
|
|
287
|
+
parser.add_argument("--repo", default=".")
|
|
288
|
+
parser.add_argument("--step", action="append", default=[], help="Acceptance step text.")
|
|
289
|
+
parser.add_argument("--self-test", action="store_true")
|
|
290
|
+
parser.add_argument("--report", action="store_true", help="JSON probe results to stdout.")
|
|
291
|
+
args = parser.parse_args()
|
|
292
|
+
repo = Path(args.repo).resolve()
|
|
293
|
+
|
|
294
|
+
if args.self_test:
|
|
295
|
+
try:
|
|
296
|
+
self_test()
|
|
297
|
+
except AssertionError as exc:
|
|
298
|
+
print(f"self-test failed: {exc}", file=sys.stderr)
|
|
299
|
+
return 2
|
|
300
|
+
print("[UAT_PROBE_LIB_SELF_TEST_OK]")
|
|
301
|
+
return 0
|
|
302
|
+
|
|
303
|
+
if args.report or args.step:
|
|
304
|
+
results = probe_steps(args.step or ["run tests"], repo)
|
|
305
|
+
print(json.dumps(results, sort_keys=True, separators=(",", ":")))
|
|
306
|
+
if any(r.get("reason_code") == UAT_PROBE_UNRESOLVED for r in results):
|
|
307
|
+
return 1
|
|
308
|
+
if any(not r.get("passed") for r in results):
|
|
309
|
+
return 1
|
|
310
|
+
return 0
|
|
311
|
+
|
|
312
|
+
parser.print_help()
|
|
313
|
+
return 2
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
if __name__ == "__main__":
|
|
317
|
+
sys.exit(main())
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Validate README feature coverage vs backlog (US-0091 / DEC-0074).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
13
|
+
_REPO_ROOT = os.path.normpath(os.path.join(_SCRIPT_DIR, ".."))
|
|
14
|
+
|
|
15
|
+
if _REPO_ROOT not in sys.path:
|
|
16
|
+
sys.path.insert(0, _REPO_ROOT)
|
|
17
|
+
if _SCRIPT_DIR not in sys.path:
|
|
18
|
+
sys.path.insert(0, _SCRIPT_DIR)
|
|
19
|
+
|
|
20
|
+
import installer # noqa: E402
|
|
21
|
+
import readme_feature_coverage_lib as rfc # noqa: E402
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _enforce_flag(merged: dict) -> bool:
|
|
25
|
+
raw = (merged.get("README_FEATURE_COVERAGE_ENFORCE") or "0").strip()
|
|
26
|
+
return raw == "1"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main() -> int:
|
|
30
|
+
parser = argparse.ArgumentParser(
|
|
31
|
+
description="Validate README feature coverage vs backlog (DEC-0074)."
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--repo",
|
|
35
|
+
default=_REPO_ROOT,
|
|
36
|
+
help="Target repository root (default: parent of scripts/).",
|
|
37
|
+
)
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"--backlog",
|
|
40
|
+
default=None,
|
|
41
|
+
help="Backlog file (default: docs/product/backlog.md under --repo).",
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"--self-test",
|
|
45
|
+
action="store_true",
|
|
46
|
+
help="Run predicate matrix + schema stability checks.",
|
|
47
|
+
)
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"--report",
|
|
50
|
+
action="store_true",
|
|
51
|
+
help="Emit stable JSON report to stdout.",
|
|
52
|
+
)
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--audit-out",
|
|
55
|
+
metavar="PATH",
|
|
56
|
+
default=None,
|
|
57
|
+
help="Write gap audit artifact (JSON) to PATH.",
|
|
58
|
+
)
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"--enforce",
|
|
61
|
+
action="store_true",
|
|
62
|
+
help="Blocking mode (required for /release when enforce=1).",
|
|
63
|
+
)
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"--no-template-parity",
|
|
66
|
+
action="store_true",
|
|
67
|
+
help="Skip active vs template/ README and script parity sub-check.",
|
|
68
|
+
)
|
|
69
|
+
args = parser.parse_args()
|
|
70
|
+
|
|
71
|
+
if args.self_test:
|
|
72
|
+
try:
|
|
73
|
+
rfc.self_test_predicate_matrix()
|
|
74
|
+
rfc.self_test_report_schema()
|
|
75
|
+
except AssertionError as exc:
|
|
76
|
+
print(f"self-test failed: {exc}", file=sys.stderr)
|
|
77
|
+
return 2
|
|
78
|
+
print("[README_FEATURE_COVERAGE_SELF_TEST_OK]")
|
|
79
|
+
return 0
|
|
80
|
+
|
|
81
|
+
target = os.path.abspath(args.repo)
|
|
82
|
+
backlog = args.backlog or os.path.join(target, "docs", "product", "backlog.md")
|
|
83
|
+
if not os.path.isfile(backlog):
|
|
84
|
+
print(f"{rfc.REASON_INPUT_INVALID}: backlog not found: {backlog}", file=sys.stderr)
|
|
85
|
+
return 2
|
|
86
|
+
|
|
87
|
+
merged, _paths = installer.merge_scratchpad_layers(target)
|
|
88
|
+
enforce = args.enforce or _enforce_flag(merged)
|
|
89
|
+
|
|
90
|
+
merged_profile = None if args.no_template_parity else merged
|
|
91
|
+
report, stderr_lines = rfc.build_report(
|
|
92
|
+
target,
|
|
93
|
+
backlog,
|
|
94
|
+
enforce=enforce,
|
|
95
|
+
merged=merged_profile,
|
|
96
|
+
skip_parity=args.no_template_parity,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if args.audit_out:
|
|
100
|
+
audit_path = args.audit_out
|
|
101
|
+
if not os.path.isabs(audit_path):
|
|
102
|
+
audit_path = os.path.join(target, audit_path)
|
|
103
|
+
os.makedirs(os.path.dirname(audit_path), exist_ok=True)
|
|
104
|
+
audit_obj = {
|
|
105
|
+
"audit_schema_version": 1,
|
|
106
|
+
"coverage_total": report["coverage_total"],
|
|
107
|
+
"gaps": report["gaps"],
|
|
108
|
+
"items": report["coverage_present"] + report["coverage_missing"],
|
|
109
|
+
"status": report["status"],
|
|
110
|
+
}
|
|
111
|
+
with open(audit_path, "w", encoding="utf-8", newline="\n") as f:
|
|
112
|
+
f.write(rfc.canonical_json(audit_obj))
|
|
113
|
+
|
|
114
|
+
blocking = bool(stderr_lines) or report["status"] != "PASS"
|
|
115
|
+
|
|
116
|
+
if args.report or not args.audit_out:
|
|
117
|
+
sys.stdout.write(rfc.canonical_json(report))
|
|
118
|
+
|
|
119
|
+
if blocking and (args.enforce or args.report):
|
|
120
|
+
print(rfc.REASON_BLOCKED, file=sys.stderr)
|
|
121
|
+
for line in sorted(set(stderr_lines)):
|
|
122
|
+
print(line, file=sys.stderr)
|
|
123
|
+
return 1
|
|
124
|
+
|
|
125
|
+
if blocking and args.audit_out and not args.report:
|
|
126
|
+
return 0
|
|
127
|
+
|
|
128
|
+
if blocking:
|
|
129
|
+
print(rfc.REASON_BLOCKED, file=sys.stderr)
|
|
130
|
+
for line in sorted(set(stderr_lines)):
|
|
131
|
+
print(line, file=sys.stderr)
|
|
132
|
+
return 1
|
|
133
|
+
|
|
134
|
+
if args.enforce:
|
|
135
|
+
print("[README_FEATURE_COVERAGE_VALIDATE_OK]")
|
|
136
|
+
return 0
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__":
|
|
140
|
+
raise SystemExit(main())
|