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.
@@ -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())