bytedigger-engine 0.1.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.
Files changed (149) hide show
  1. ac_dsl.py +552 -0
  2. audit_gate.py +237 -0
  3. bytedigger_engine-0.1.0.dist-info/METADATA +186 -0
  4. bytedigger_engine-0.1.0.dist-info/RECORD +149 -0
  5. bytedigger_engine-0.1.0.dist-info/WHEEL +5 -0
  6. bytedigger_engine-0.1.0.dist-info/entry_points.txt +2 -0
  7. bytedigger_engine-0.1.0.dist-info/licenses/LICENSE +21 -0
  8. bytedigger_engine-0.1.0.dist-info/top_level.txt +46 -0
  9. config_provider.py +225 -0
  10. contracts.py +672 -0
  11. corpus_extract.py +122 -0
  12. derive_state.py +303 -0
  13. engine.py +1042 -0
  14. error_codes.py +300 -0
  15. error_locus.py +126 -0
  16. event_log.py +151 -0
  17. event_sink.py +87 -0
  18. execution_provenance.py +50 -0
  19. findings_sidecar.py +43 -0
  20. flags_catalog.py +552 -0
  21. forbidden_import.py +93 -0
  22. format_conversion_cases.py +65 -0
  23. helper_extraction.py +94 -0
  24. io_utils.py +16 -0
  25. lib/__init__.py +0 -0
  26. lib/authored_boundary.py +225 -0
  27. lib/bounded_spawn.py +41 -0
  28. lib/constitution_loader.py +211 -0
  29. lib/cost_rollup.py +105 -0
  30. lib/dbos_list_runs.py +81 -0
  31. lib/dbos_setup.py +428 -0
  32. lib/dbos_status_run.py +76 -0
  33. lib/directed_repair.py +626 -0
  34. lib/env_limit.py +36 -0
  35. lib/git_cwd.py +71 -0
  36. lib/git_port.py +210 -0
  37. lib/git_write_port.py +96 -0
  38. lib/llm_provider.py +145 -0
  39. lib/model_config.py +193 -0
  40. lib/mypy_baseline.py +88 -0
  41. lib/observability/emit_resolver.py +40 -0
  42. lib/phase_sentinel.py +326 -0
  43. lib/plugins/__init__.py +0 -0
  44. lib/plugins/anti_hallucination/__init__.py +1 -0
  45. lib/plugins/anti_hallucination/config.yaml +31 -0
  46. lib/plugins/anti_hallucination/helper.py +607 -0
  47. lib/plugins/anti_hallucination/producer_prompt_fragment.md +15 -0
  48. lib/plugins/anti_hallucination/prompt_fragment.md +21 -0
  49. lib/plugins/anti_hallucination/semantic_verifier.py +589 -0
  50. lib/plugins/checklist_convergence/__init__.py +27 -0
  51. lib/plugins/checklist_convergence/delta_retry_prompt.py +51 -0
  52. lib/plugins/checklist_convergence/delta_reviewer_prompt.py +165 -0
  53. lib/plugins/checklist_convergence/findings_extractor.py +150 -0
  54. lib/plugins/checklist_convergence/impl_delta_retry_prompt.py +57 -0
  55. lib/plugins/checklist_convergence/restricted_reviewer_prompt.py +58 -0
  56. lib/plugins/checklist_convergence/restricted_writer_prompt.py +66 -0
  57. lib/plugins/checklist_convergence/surgical_revise.py +157 -0
  58. lib/plugins/checklist_convergence/verdict_parser.py +85 -0
  59. lib/plugins/disk_truth/__init__.py +20 -0
  60. lib/plugins/disk_truth/git_diff.py +190 -0
  61. lib/plugins/disk_truth/schema.py +152 -0
  62. lib/plugins/disk_truth/suite_boyscout.py +189 -0
  63. lib/plugins/disk_truth/test_runner.py +256 -0
  64. lib/plugins/disk_truth/verdict_parser.py +77 -0
  65. lib/plugins/review_schema/__init__.py +16 -0
  66. lib/plugins/review_schema/canonical.py +89 -0
  67. lib/project_root.py +103 -0
  68. lib/recoverable_gate.py +134 -0
  69. lib/reference_backends/__init__.py +6 -0
  70. lib/reference_backends/agent_sdk.py +292 -0
  71. lib/reference_backends/anthropic_api.py +299 -0
  72. lib/reference_backends/anthropic_oauth.py +126 -0
  73. lib/reference_backends/pydantic_anthropic.py +295 -0
  74. lib/reference_backends/pydantic_openai.py +737 -0
  75. lib/restart_governor.py +314 -0
  76. lib/resume_keying.py +33 -0
  77. lib/revise_counter.py +78 -0
  78. lib/run_allowlist.py +417 -0
  79. lib/spec_defect_ledger.py +178 -0
  80. lib/spec_retry_cycle.py +119 -0
  81. lib/step_sentinel.py +182 -0
  82. lib/timeout_policy.py +198 -0
  83. lib/util/__init__.py +0 -0
  84. lib/util/path_classifier.py +44 -0
  85. lib/verdict_parse.py +175 -0
  86. lib/verdict_resolution.py +52 -0
  87. lib/worktree_root.py +24 -0
  88. llm_subprocess.py +2694 -0
  89. net_new_delta.py +78 -0
  90. precommit_lints.py +26 -0
  91. presence_triad.py +94 -0
  92. red_lint_checks.py +69 -0
  93. reentry_ac.py +97 -0
  94. reject_log.py +185 -0
  95. reject_stats.py +73 -0
  96. reproducibility.py +69 -0
  97. resume_key_lint.py +244 -0
  98. run.py +244 -0
  99. scope_inverse.py +35 -0
  100. scripts/__init__.py +0 -0
  101. scripts/red_lint/__init__.py +0 -0
  102. scripts/red_lint/rules.yml +125 -0
  103. security/__init__.py +3 -0
  104. security/except-pass-allowlist.txt +27 -0
  105. security/gitleaks.toml +4 -0
  106. security/secure-codegen-fragment.md +8 -0
  107. security/secure-codegen-rules.md +62 -0
  108. security/security_lint.py +254 -0
  109. security/semgrep-rules.yml +191 -0
  110. skip_logic.py +221 -0
  111. spec_cite.py +459 -0
  112. spec_class.py +49 -0
  113. spec_coverage.py +272 -0
  114. stub_passability.py +193 -0
  115. suite_safety.py +73 -0
  116. telemetry_ctx.py +89 -0
  117. tier_gate.py +117 -0
  118. token_consistency.py +55 -0
  119. verdict_anchor.py +205 -0
  120. verdict_gate.py +75 -0
  121. verdict_verify.py +470 -0
  122. workflows/__init__.py +46 -0
  123. workflows/_baseline_delta.py +79 -0
  124. workflows/_recoverable_policy.py +98 -0
  125. workflows/_standards_context.py +103 -0
  126. workflows/_task_description.py +25 -0
  127. workflows/echo.py +18 -0
  128. workflows/graph_source.py +126 -0
  129. workflows/phase_05_inject.py +1086 -0
  130. workflows/phase_0_6_artifact_detect.py +137 -0
  131. workflows/phase_0_research.py +86 -0
  132. workflows/phase_1_discovery.py +543 -0
  133. workflows/phase_2_explore.py +492 -0
  134. workflows/phase_3_clarify.py +380 -0
  135. workflows/phase_45_spec.py +4212 -0
  136. workflows/phase_45_spec_lite.py +1289 -0
  137. workflows/phase_4_architect.py +407 -0
  138. workflows/phase_5_devops_scan.py +391 -0
  139. workflows/phase_5_implement.py +5958 -0
  140. workflows/phase_5_integration_canary.py +233 -0
  141. workflows/phase_5_integrity.py +579 -0
  142. workflows/phase_6_fix_integrity.py +635 -0
  143. workflows/phase_6_review.py +4975 -0
  144. workflows/phase_6_review_simple_fastpath.py +95 -0
  145. workflows/phase_6_smoke.py +119 -0
  146. workflows/phase_7_synthesize.py +739 -0
  147. workflows/phase_8_post_deploy.py +1590 -0
  148. workflows/phase_devops_pipeline.py +69 -0
  149. workflows/phase_workflows_common.py +504 -0
ac_dsl.py ADDED
@@ -0,0 +1,552 @@
1
+ """ac_dsl.py — AC-checks DSL: schema, parser, admission, mechanical runner.
2
+
3
+ Ship: GH517 Ship A1 (frozen spec: Decisions/
4
+ 2026-07-11_GH517A1_ac_dsl_ship_spec.md). Sole source of the `### AC-checks`
5
+ schema — the registry `CHECK_TYPES` mirrors the pattern of
6
+ `flags_catalog.FLAGS` / `error_codes.ERROR_CODES` (hand-written dict, closed
7
+ by AC1's registry-closure test).
8
+
9
+ No env reads at import time. `verdict_verify` is imported as a plain sibling
10
+ module (both live in engine_py/, both are on sys.path via conftest.py /
11
+ the spec-compile.py CLI shim).
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import os
17
+ import re
18
+ import subprocess
19
+ from dataclasses import dataclass, field
20
+ from pathlib import Path
21
+
22
+ import verdict_verify
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # §1.1 CHECK_TYPES registry
26
+ # ---------------------------------------------------------------------------
27
+
28
+
29
+ def _req(**extra):
30
+ d = {"required": True}
31
+ d.update(extra)
32
+ return d
33
+
34
+
35
+ def _opt(**extra):
36
+ d = {"required": False}
37
+ d.update(extra)
38
+ return d
39
+
40
+
41
+ CHECK_TYPES: dict[str, dict] = {
42
+ "file_contains": {
43
+ "description": "Assert a file on disk does/doesn't contain a pattern.",
44
+ "params": {
45
+ "path": _req(),
46
+ "pattern": _opt(),
47
+ "kind": _opt(enum=["fixed", "regex"], default="fixed"),
48
+ "expect": _opt(enum=["present", "absent"], default="present"),
49
+ },
50
+ },
51
+ "cmd_exit": {
52
+ "description": "Run a command and assert its exit code (and optional stdout/stderr regex).",
53
+ "params": {
54
+ "argv": _req(),
55
+ "cwd": _opt(),
56
+ "env": _opt(),
57
+ "exit_code": _req(),
58
+ "stdout_re": _opt(),
59
+ "stderr_re": _opt(),
60
+ "timeout_s": _opt(default=60),
61
+ },
62
+ },
63
+ "event_emitted": {
64
+ "description": "Assert a named event appears (or is absent) in a jsonl stream.",
65
+ "params": {
66
+ "stream": _req(),
67
+ "name": _req(),
68
+ "fields_subset": _opt(),
69
+ "min_count": _opt(default=1),
70
+ },
71
+ },
72
+ "json_field": {
73
+ "description": "Assert a dot-path field in a JSON/JSONL file equals/matches/is-absent.",
74
+ "params": {
75
+ "file": _req(),
76
+ "pointer": _req(),
77
+ "op": _req(enum=["equals", "matches", "absent"]),
78
+ "value": _opt(),
79
+ },
80
+ },
81
+ "sha_anchor": {
82
+ "description": "Assert a verdict-anchor block verifies and covers the given files.",
83
+ "params": {
84
+ "doc": _req(),
85
+ "files": _req(),
86
+ },
87
+ },
88
+ "count_delta": {
89
+ "description": "Assert a counter (cmd stdout or jsonl line count) moved by expected_delta.",
90
+ "params": {
91
+ "counter": _req(),
92
+ "kind": _req(enum=["cmd", "stream"]),
93
+ "baseline": _req(),
94
+ "expected_delta": _req(),
95
+ },
96
+ },
97
+ "monotonic": {
98
+ "description": "Assert a field across a jsonl stream is non-decreasing.",
99
+ "params": {
100
+ "stream": _req(),
101
+ "field": _req(),
102
+ },
103
+ },
104
+ "llm_judged": {
105
+ "description": "Defer to LLM judgement against a rubric (not mechanically executed).",
106
+ "params": {
107
+ "rubric": _req(),
108
+ },
109
+ },
110
+ }
111
+
112
+
113
+ # ---------------------------------------------------------------------------
114
+ # Dataclasses
115
+ # ---------------------------------------------------------------------------
116
+
117
+
118
+ @dataclass
119
+ class CheckSpec:
120
+ ac_id: str
121
+ type: str
122
+ params: dict
123
+
124
+
125
+ @dataclass
126
+ class AdmissionReport:
127
+ result: str
128
+ per_ac: dict = field(default_factory=dict)
129
+ reasons: list = field(default_factory=list)
130
+
131
+
132
+ @dataclass
133
+ class CheckResult:
134
+ ac_id: str
135
+ type: str
136
+ status: str
137
+ reason: str
138
+
139
+
140
+ # ---------------------------------------------------------------------------
141
+ # AC-id normalization (local, to avoid importing verdict_verify's private symbol)
142
+ # ---------------------------------------------------------------------------
143
+
144
+
145
+ def _normalize_ac_id(raw: str) -> str:
146
+ raw = str(raw).strip()
147
+ if raw.upper().startswith("AC"):
148
+ return "AC" + raw[2:]
149
+ return f"AC{raw}"
150
+
151
+
152
+ _AC_CHECKS_HEADER_RE = re.compile(r"^### AC-checks\s*$", re.MULTILINE)
153
+ _NEXT_HEADING_RE = re.compile(r"^#{1,3}\s")
154
+ _YAML_FENCE_RE = re.compile(r"```yaml\s*\n(.*?)```", re.DOTALL)
155
+
156
+
157
+ def parse_ac_checks(spec_text: str) -> dict[str, CheckSpec]:
158
+ """Parse the `### AC-checks` section's single yaml block into CheckSpecs."""
159
+ header_match = _AC_CHECKS_HEADER_RE.search(spec_text)
160
+ if not header_match:
161
+ raise ValueError("AC-checks section not found (heading '### AC-checks' missing)")
162
+
163
+ lines = spec_text.splitlines()
164
+ # find the line index of the header
165
+ header_line_idx = spec_text.count("\n", 0, header_match.start())
166
+
167
+ end_idx = len(lines)
168
+ for i in range(header_line_idx + 1, len(lines)):
169
+ if _NEXT_HEADING_RE.match(lines[i]):
170
+ end_idx = i
171
+ break
172
+
173
+ section_text = "\n".join(lines[header_line_idx:end_idx])
174
+
175
+ fence_match = _YAML_FENCE_RE.search(section_text)
176
+ if fence_match:
177
+ yaml_text = fence_match.group(1)
178
+ else:
179
+ # bare yaml: everything after the header line
180
+ yaml_text = "\n".join(lines[header_line_idx + 1:end_idx])
181
+
182
+ # lazy import: PyYAML отсутствует в чистых uvx-окружениях (canary.sh гоняет
183
+ # spec-тесты через `uvx --from pytest`); top-level import ронял collection
184
+ # 12 модулей через цепочку phase_45_spec → ac_dsl
185
+ import yaml
186
+
187
+ try:
188
+ data = yaml.safe_load(yaml_text)
189
+ except yaml.YAMLError as exc:
190
+ raise ValueError(f"AC-checks yaml unparseable: {exc}") from exc
191
+
192
+ if not isinstance(data, dict):
193
+ raise ValueError("AC-checks yaml root is not a mapping")
194
+
195
+ out: dict[str, CheckSpec] = {}
196
+ for raw_id, block in data.items():
197
+ ac_id = _normalize_ac_id(raw_id)
198
+ if not isinstance(block, dict):
199
+ raise ValueError(f"AC-checks block for {raw_id} is not a mapping")
200
+ block = dict(block)
201
+ ctype = block.pop("type", None)
202
+ out[ac_id] = CheckSpec(ac_id=ac_id, type=ctype, params=block)
203
+ return out
204
+
205
+
206
+ # ---------------------------------------------------------------------------
207
+ # admit
208
+ # ---------------------------------------------------------------------------
209
+
210
+
211
+ def admit(spec_text: str) -> AdmissionReport:
212
+ per_ac: dict = {}
213
+ reasons: list = []
214
+
215
+ spec_ac_ids = verdict_verify.parse_spec_ac_ids(spec_text)
216
+ if not spec_ac_ids:
217
+ return AdmissionReport(result="REJECT", per_ac={}, reasons=["no ACs found"])
218
+
219
+ try:
220
+ checks = parse_ac_checks(spec_text)
221
+ except ValueError as exc:
222
+ return AdmissionReport(
223
+ result="REJECT",
224
+ per_ac={},
225
+ reasons=[f"AC-checks section missing/unparseable: {exc}"],
226
+ )
227
+
228
+ check_ids = set(checks.keys())
229
+
230
+ for ac_id in spec_ac_ids:
231
+ if ac_id not in check_ids:
232
+ per_ac[ac_id] = {"status": "REJECT", "reason": "missing check block"}
233
+
234
+ for ac_id in check_ids:
235
+ if ac_id not in spec_ac_ids:
236
+ per_ac[ac_id] = {"status": "REJECT", "reason": "orphan check block (no §3 AC)"}
237
+ reasons.append(f"orphan check block (no §3 AC): {ac_id}")
238
+
239
+ # Type + param validation for every AC that has a check block and is not
240
+ # already rejected as orphan-only (orphan ACs still get validated too, but
241
+ # their status is already REJECT — validation may add more detail via reasons).
242
+ for ac_id, spec in checks.items():
243
+ if ac_id in per_ac and per_ac[ac_id]["status"] == "REJECT" and "orphan" in per_ac[ac_id]["reason"]:
244
+ # still validate for informative reasons, but don't overwrite orphan status
245
+ pass
246
+
247
+ ctype = spec.type
248
+ if ctype is None or ctype not in CHECK_TYPES:
249
+ reason = f"unknown check type: {ctype}"
250
+ if ac_id not in per_ac or per_ac[ac_id]["status"] != "REJECT":
251
+ per_ac[ac_id] = {"status": "REJECT", "reason": reason}
252
+ continue
253
+
254
+ type_schema = CHECK_TYPES[ctype]["params"]
255
+ param_reason = None
256
+ for pname in spec.params:
257
+ if pname not in type_schema:
258
+ param_reason = f"unknown parameter: {pname}"
259
+ break
260
+ if param_reason is None:
261
+ for pname, pschema in type_schema.items():
262
+ if pschema.get("required") and pname not in spec.params:
263
+ param_reason = f"missing required parameter: {pname}"
264
+ break
265
+ if param_reason is not None:
266
+ if ac_id not in per_ac or per_ac[ac_id]["status"] != "REJECT":
267
+ per_ac[ac_id] = {"status": "REJECT", "reason": param_reason}
268
+ continue
269
+
270
+ if ctype == "llm_judged":
271
+ rubric = spec.params.get("rubric")
272
+ if not isinstance(rubric, str) or not rubric.strip():
273
+ if ac_id not in per_ac or per_ac[ac_id]["status"] != "REJECT":
274
+ per_ac[ac_id] = {"status": "REJECT", "reason": "llm_judged without rubric"}
275
+ continue
276
+
277
+ # Presence-gate: event_emitted with min_count==0 requires a paired positive
278
+ # (min_count>=1, same stream) somewhere in the check set.
279
+ for ac_id, spec in checks.items():
280
+ if spec.type != "event_emitted":
281
+ continue
282
+ if ac_id in per_ac and per_ac[ac_id]["status"] == "REJECT":
283
+ continue
284
+ min_count = spec.params.get("min_count", 1)
285
+ if min_count != 0:
286
+ continue
287
+ stream = spec.params.get("stream")
288
+ has_pair = False
289
+ for other_id, other in checks.items():
290
+ if other_id == ac_id or other.type != "event_emitted":
291
+ continue
292
+ other_min = other.params.get("min_count", 1)
293
+ if other.params.get("stream") == stream and other_min != 0:
294
+ has_pair = True
295
+ break
296
+ if not has_pair:
297
+ per_ac[ac_id] = {
298
+ "status": "REJECT",
299
+ "reason": "absence check without paired positive",
300
+ }
301
+
302
+ for ac_id in check_ids:
303
+ if ac_id in spec_ac_ids and ac_id not in per_ac:
304
+ per_ac[ac_id] = {"status": "OK", "reason": ""}
305
+
306
+ result = "ACCEPT" if all(v["status"] == "OK" for v in per_ac.values()) else "REJECT"
307
+ return AdmissionReport(result=result, per_ac=per_ac, reasons=reasons)
308
+
309
+
310
+ # ---------------------------------------------------------------------------
311
+ # run_checks — mechanical executors
312
+ # ---------------------------------------------------------------------------
313
+
314
+
315
+ def _read_jsonl_objs(path: Path) -> list[dict]:
316
+ objs = []
317
+ for line in path.read_text(encoding="utf-8").splitlines():
318
+ line = line.strip()
319
+ if not line:
320
+ continue
321
+ try:
322
+ obj = json.loads(line)
323
+ except (json.JSONDecodeError, ValueError):
324
+ continue
325
+ if isinstance(obj, dict):
326
+ objs.append(obj)
327
+ return objs
328
+
329
+
330
+ def _exec_file_contains(params: dict, repo_root: Path) -> tuple[str, str]:
331
+ path = repo_root / params["path"]
332
+ pattern = params.get("pattern")
333
+ kind = params.get("kind", "fixed")
334
+ expect = params.get("expect", "present")
335
+
336
+ if pattern is None:
337
+ found = path.exists()
338
+ else:
339
+ if not path.exists():
340
+ found = False
341
+ else:
342
+ text = path.read_text(encoding="utf-8")
343
+ if kind == "regex":
344
+ found = re.search(pattern, text) is not None
345
+ else:
346
+ found = pattern in text
347
+
348
+ ok = found if expect == "present" else not found
349
+ return ("PASS", "") if ok else ("FAIL", f"file_contains expect={expect} found={found}")
350
+
351
+
352
+ def _exec_cmd_exit(params: dict, repo_root: Path) -> tuple[str, str]:
353
+ argv = params["argv"]
354
+ cwd = repo_root / params["cwd"] if params.get("cwd") else repo_root
355
+ env_overlay = params.get("env") or {}
356
+ run_env = dict(os.environ)
357
+ run_env.update(env_overlay)
358
+ timeout_s = params.get("timeout_s", 60)
359
+ proc = subprocess.run(
360
+ argv, cwd=str(cwd), env=run_env, timeout=timeout_s,
361
+ capture_output=True, text=True,
362
+ )
363
+ ok = proc.returncode == params["exit_code"]
364
+ if ok and params.get("stdout_re"):
365
+ ok = re.search(params["stdout_re"], proc.stdout) is not None
366
+ if ok and params.get("stderr_re"):
367
+ ok = re.search(params["stderr_re"], proc.stderr) is not None
368
+ if ok:
369
+ return ("PASS", "")
370
+ return ("FAIL", f"cmd_exit returncode={proc.returncode} expected={params['exit_code']}")
371
+
372
+
373
+ def _exec_event_emitted(params: dict, repo_root: Path) -> tuple[str, str]:
374
+ stream = repo_root / params["stream"]
375
+ name = params["name"]
376
+ fields_subset = params.get("fields_subset") or {}
377
+ min_count = params.get("min_count", 1)
378
+
379
+ if not stream.exists():
380
+ if min_count == 0:
381
+ return ("PASS", "")
382
+ return ("FAIL", f"event_emitted: stream not found: {stream}")
383
+
384
+ try:
385
+ objs = _read_jsonl_objs(stream)
386
+ except OSError as exc:
387
+ if min_count == 0:
388
+ return ("PASS", "")
389
+ return ("FAIL", f"driver: {exc}")
390
+
391
+ count = 0
392
+ for obj in objs:
393
+ if obj.get("name") != name:
394
+ continue
395
+ if all(obj.get(k) == v for k, v in fields_subset.items()):
396
+ count += 1
397
+
398
+ if min_count == 0:
399
+ ok = count == 0
400
+ else:
401
+ ok = count >= min_count
402
+ if ok:
403
+ return ("PASS", "")
404
+ return ("FAIL", f"event_emitted count={count} min_count={min_count}")
405
+
406
+
407
+ def _resolve_pointer(obj, pointer: str):
408
+ node = obj
409
+ for seg in pointer.split("."):
410
+ if isinstance(node, list):
411
+ idx = int(seg)
412
+ node = node[idx]
413
+ elif isinstance(node, dict):
414
+ if seg not in node:
415
+ raise KeyError(seg)
416
+ node = node[seg]
417
+ else:
418
+ raise KeyError(seg)
419
+ return node
420
+
421
+
422
+ def _exec_json_field(params: dict, repo_root: Path) -> tuple[str, str]:
423
+ path = repo_root / params["file"]
424
+ pointer = params["pointer"]
425
+ op = params["op"]
426
+ value = params.get("value")
427
+
428
+ if str(path).endswith(".jsonl"):
429
+ lines = [ln for ln in path.read_text(encoding="utf-8").splitlines() if ln.strip()]
430
+ text = lines[-1] if lines else "{}"
431
+ data = json.loads(text)
432
+ else:
433
+ data = json.loads(path.read_text(encoding="utf-8"))
434
+
435
+ try:
436
+ node = _resolve_pointer(data, pointer)
437
+ resolved = True
438
+ except (KeyError, IndexError, ValueError, TypeError):
439
+ node = None
440
+ resolved = False
441
+
442
+ if op == "absent":
443
+ ok = not resolved
444
+ elif op == "equals":
445
+ ok = resolved and node == value
446
+ elif op == "matches":
447
+ ok = resolved and re.search(str(value), str(node)) is not None
448
+ else:
449
+ ok = False
450
+
451
+ if ok:
452
+ return ("PASS", "")
453
+ return ("FAIL", f"json_field op={op} resolved={resolved} node={node!r}")
454
+
455
+
456
+ def _exec_sha_anchor(params: dict, repo_root: Path) -> tuple[str, str]:
457
+ doc = repo_root / params["doc"]
458
+ text = doc.read_text(encoding="utf-8")
459
+ result = verdict_verify.verify_anchor(text, repo_root)
460
+ if result.result != "VERIFIED":
461
+ return ("FAIL", f"sha_anchor result={result.result} reason={result.reason}")
462
+
463
+ anchor_paths = {relpath for _kind, relpath, _sha in verdict_verify.parse_anchor(text)}
464
+ for f in params["files"]:
465
+ if f not in anchor_paths:
466
+ return ("FAIL", f"sha_anchor: file {f} not present in anchor block")
467
+ return ("PASS", "")
468
+
469
+
470
+ def _exec_count_delta(params: dict, repo_root: Path) -> tuple[str, str]:
471
+ kind = params["kind"]
472
+ counter = params["counter"]
473
+ baseline = params["baseline"]
474
+ expected_delta = params["expected_delta"]
475
+
476
+ if kind == "cmd":
477
+ proc = subprocess.run(
478
+ counter, cwd=str(repo_root), capture_output=True, text=True, timeout=60
479
+ )
480
+ m = re.search(r"-?\d+", proc.stdout)
481
+ current = int(m.group(0)) if m else 0
482
+ else:
483
+ path = repo_root / counter
484
+ lines = [ln for ln in path.read_text(encoding="utf-8").splitlines() if ln.strip()]
485
+ current = len(lines)
486
+
487
+ delta = current - baseline
488
+ if delta == expected_delta:
489
+ return ("PASS", "")
490
+ return ("FAIL", f"count_delta current={current} baseline={baseline} delta={delta} expected={expected_delta}")
491
+
492
+
493
+ def _exec_monotonic(params: dict, repo_root: Path) -> tuple[str, str]:
494
+ stream = repo_root / params["stream"]
495
+ field_name = params["field"]
496
+ objs = _read_jsonl_objs(stream)
497
+ values = [obj[field_name] for obj in objs if field_name in obj]
498
+ if not values:
499
+ return ("FAIL", "monotonic: no values found")
500
+ ok = all(values[i] <= values[i + 1] for i in range(len(values) - 1))
501
+ if ok:
502
+ return ("PASS", "")
503
+ return ("FAIL", f"monotonic: not non-decreasing: {values}")
504
+
505
+
506
+ _EXECUTORS = {
507
+ "file_contains": _exec_file_contains,
508
+ "cmd_exit": _exec_cmd_exit,
509
+ "event_emitted": _exec_event_emitted,
510
+ "json_field": _exec_json_field,
511
+ "sha_anchor": _exec_sha_anchor,
512
+ "count_delta": _exec_count_delta,
513
+ "monotonic": _exec_monotonic,
514
+ }
515
+
516
+
517
+ def run_checks(checks: dict, repo_root, env=None) -> list:
518
+ repo_root = Path(repo_root)
519
+ results = []
520
+ for ac_id, spec in checks.items():
521
+ if spec.type == "llm_judged":
522
+ results.append(CheckResult(ac_id=ac_id, type=spec.type, status="JUDGE", reason=""))
523
+ continue
524
+ executor = _EXECUTORS.get(spec.type)
525
+ if executor is None:
526
+ results.append(
527
+ CheckResult(ac_id=ac_id, type=spec.type, status="FAIL", reason=f"unknown type: {spec.type}")
528
+ )
529
+ continue
530
+ try:
531
+ status, reason = executor(spec.params, repo_root)
532
+ except Exception as exc: # noqa: BLE001 - fail-closed driver errors (§lesson 221)
533
+ status, reason = "FAIL", f"driver: {exc}"
534
+ results.append(CheckResult(ac_id=ac_id, type=spec.type, status=status, reason=reason))
535
+ return results
536
+
537
+
538
+ # ---------------------------------------------------------------------------
539
+ # render_checklist
540
+ # ---------------------------------------------------------------------------
541
+
542
+
543
+ def render_checklist(results: list) -> str:
544
+ lines = []
545
+ for r in results:
546
+ if r.status == "JUDGE":
547
+ lines.append(f"- {r.ac_id}: LLM_JUDGED — rubric deferred")
548
+ elif r.status == "PASS":
549
+ lines.append(f"- {r.ac_id}: PASS")
550
+ else:
551
+ lines.append(f"- {r.ac_id}: FAIL ({r.reason})")
552
+ return "\n".join(lines) + ("\n" if lines else "")