hunter-harness 0.1.3 → 0.2.1

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. package/LICENSE +21 -21
  2. package/README.md +11 -0
  3. package/dist/bin.js +1540 -491
  4. package/package.json +9 -7
  5. package/resources/harness/general/.harness-build.json +0 -5
  6. package/resources/harness/general/CONTEXT.md +0 -65
  7. package/resources/harness/general/README.md +0 -478
  8. package/resources/harness/general/agents/harness-evaluator.md +0 -96
  9. package/resources/harness/general/agents/harness-explorer.md +0 -72
  10. package/resources/harness/general/agents/harness-reviewer.md +0 -69
  11. package/resources/harness/general/harness-archive/SKILL.md +0 -215
  12. package/resources/harness/general/harness-archive/checklist.md +0 -52
  13. package/resources/harness/general/harness-archive/reference.md +0 -103
  14. package/resources/harness/general/harness-archive/scripts/gen-manifest.ps1 +0 -31
  15. package/resources/harness/general/harness-archive/templates/render-summary.mjs +0 -171
  16. package/resources/harness/general/harness-archive/templates/summary-data-template.json +0 -128
  17. package/resources/harness/general/harness-codebase-map/SKILL.md +0 -112
  18. package/resources/harness/general/harness-codebase-map/checklist.md +0 -126
  19. package/resources/harness/general/harness-codebase-map/reference.md +0 -364
  20. package/resources/harness/general/harness-codebase-map/templates/ARCHITECTURE.md +0 -57
  21. package/resources/harness/general/harness-codebase-map/templates/CONCERNS.md +0 -49
  22. package/resources/harness/general/harness-codebase-map/templates/CONVENTIONS.md +0 -57
  23. package/resources/harness/general/harness-codebase-map/templates/INTEGRATIONS.md +0 -52
  24. package/resources/harness/general/harness-codebase-map/templates/STACK.md +0 -55
  25. package/resources/harness/general/harness-codebase-map/templates/STRUCTURE.md +0 -69
  26. package/resources/harness/general/harness-codebase-map/templates/TESTING.md +0 -64
  27. package/resources/harness/general/harness-codebase-map/templates/map-manifest.schema.json +0 -73
  28. package/resources/harness/general/harness-codebase-map/templates/map-summary.md +0 -64
  29. package/resources/harness/general/harness-knowledge-ingest/SKILL.md +0 -246
  30. package/resources/harness/general/harness-knowledge-ingest/design.md +0 -842
  31. package/resources/harness/general/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -42
  32. package/resources/harness/general/harness-knowledge-ingest/mcp-config.example.json +0 -10
  33. package/resources/harness/general/harness-knowledge-ingest/reference.md +0 -309
  34. package/resources/harness/general/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -4082
  35. package/resources/harness/general/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -273
  36. package/resources/harness/general/harness-knowledge-query/SKILL.md +0 -164
  37. package/resources/harness/general/harness-plan/SKILL.md +0 -127
  38. package/resources/harness/general/harness-plan/checklist.md +0 -243
  39. package/resources/harness/general/harness-plan/protocols.md +0 -124
  40. package/resources/harness/general/harness-plan/reference.md +0 -376
  41. package/resources/harness/general/harness-review/SKILL.md +0 -156
  42. package/resources/harness/general/harness-review/checklist.md +0 -124
  43. package/resources/harness/general/harness-review/protocols.md +0 -68
  44. package/resources/harness/general/harness-review/reference.md +0 -86
  45. package/resources/harness/general/harness-run/SKILL.md +0 -132
  46. package/resources/harness/general/harness-run/checklist.md +0 -259
  47. package/resources/harness/general/harness-run/protocols.md +0 -78
  48. package/resources/harness/general/harness-run/reference.md +0 -834
  49. package/resources/harness/general/harness-submit/SKILL.md +0 -159
  50. package/resources/harness/general/harness-submit/checklist.md +0 -407
  51. package/resources/harness/general/harness-submit/reference.md +0 -152
  52. package/resources/harness/general/harness-sync/SKILL.md +0 -82
  53. package/resources/harness/general/harness-sync/reference.md +0 -153
  54. package/resources/harness/general/harness-test/SKILL.md +0 -180
  55. package/resources/harness/general/harness-test/checklist.md +0 -319
  56. package/resources/harness/general/harness-test/pitfalls.md +0 -260
  57. package/resources/harness/general/harness-test/reference.md +0 -791
  58. package/resources/harness/general/protocols/archive-report-protocol.md +0 -175
  59. package/resources/harness/general/protocols/evidence-based-reporting-protocol.md +0 -134
  60. package/resources/harness/general/protocols/ledger-protocol.md +0 -270
  61. package/resources/harness/general/protocols/powershell-protocol.md +0 -181
  62. package/resources/harness/general/protocols/report-pipeline-protocol.md +0 -150
  63. package/resources/harness/general/protocols/sensitive-info-protocol.md +0 -92
  64. package/resources/harness/general/protocols/state-layout-protocol.md +0 -131
  65. package/resources/harness/general/protocols/submit-protocol.md +0 -78
  66. package/resources/harness/general/scripts/harness_archive.py +0 -2319
  67. package/resources/harness/general/scripts/harness_deploy.py +0 -613
  68. package/resources/harness/general/scripts/harness_events.py +0 -680
  69. package/resources/harness/general/scripts/harness_ledger.py +0 -671
  70. package/resources/harness/general/scripts/harness_preflight.py +0 -780
  71. package/resources/harness/general/scripts/harness_service.py +0 -1331
  72. package/resources/harness/java/.harness-build.json +0 -5
  73. package/resources/harness/java/CONTEXT.md +0 -65
  74. package/resources/harness/java/README.md +0 -478
  75. package/resources/harness/java/agents/harness-evaluator.md +0 -96
  76. package/resources/harness/java/agents/harness-explorer.md +0 -72
  77. package/resources/harness/java/agents/harness-reviewer.md +0 -69
  78. package/resources/harness/java/harness-apidoc/SKILL.md +0 -86
  79. package/resources/harness/java/harness-apidoc/checklist.md +0 -142
  80. package/resources/harness/java/harness-apidoc/reference.md +0 -233
  81. package/resources/harness/java/harness-archive/SKILL.md +0 -215
  82. package/resources/harness/java/harness-archive/checklist.md +0 -52
  83. package/resources/harness/java/harness-archive/reference.md +0 -103
  84. package/resources/harness/java/harness-archive/scripts/gen-manifest.ps1 +0 -31
  85. package/resources/harness/java/harness-archive/templates/render-summary.mjs +0 -171
  86. package/resources/harness/java/harness-archive/templates/summary-data-template.json +0 -128
  87. package/resources/harness/java/harness-codebase-map/SKILL.md +0 -112
  88. package/resources/harness/java/harness-codebase-map/checklist.md +0 -126
  89. package/resources/harness/java/harness-codebase-map/reference.md +0 -364
  90. package/resources/harness/java/harness-codebase-map/templates/ARCHITECTURE.md +0 -57
  91. package/resources/harness/java/harness-codebase-map/templates/CONCERNS.md +0 -49
  92. package/resources/harness/java/harness-codebase-map/templates/CONVENTIONS.md +0 -57
  93. package/resources/harness/java/harness-codebase-map/templates/INTEGRATIONS.md +0 -52
  94. package/resources/harness/java/harness-codebase-map/templates/STACK.md +0 -55
  95. package/resources/harness/java/harness-codebase-map/templates/STRUCTURE.md +0 -69
  96. package/resources/harness/java/harness-codebase-map/templates/TESTING.md +0 -64
  97. package/resources/harness/java/harness-codebase-map/templates/map-manifest.schema.json +0 -73
  98. package/resources/harness/java/harness-codebase-map/templates/map-summary.md +0 -64
  99. package/resources/harness/java/harness-knowledge-ingest/SKILL.md +0 -246
  100. package/resources/harness/java/harness-knowledge-ingest/design.md +0 -842
  101. package/resources/harness/java/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -42
  102. package/resources/harness/java/harness-knowledge-ingest/mcp-config.example.json +0 -10
  103. package/resources/harness/java/harness-knowledge-ingest/reference.md +0 -309
  104. package/resources/harness/java/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -4082
  105. package/resources/harness/java/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -273
  106. package/resources/harness/java/harness-knowledge-query/SKILL.md +0 -164
  107. package/resources/harness/java/harness-package/SKILL.md +0 -87
  108. package/resources/harness/java/harness-package/checklist.md +0 -322
  109. package/resources/harness/java/harness-package/reference.md +0 -210
  110. package/resources/harness/java/harness-plan/SKILL.md +0 -127
  111. package/resources/harness/java/harness-plan/checklist.md +0 -243
  112. package/resources/harness/java/harness-plan/protocols.md +0 -124
  113. package/resources/harness/java/harness-plan/reference.md +0 -376
  114. package/resources/harness/java/harness-review/SKILL.md +0 -156
  115. package/resources/harness/java/harness-review/checklist.md +0 -124
  116. package/resources/harness/java/harness-review/protocols.md +0 -68
  117. package/resources/harness/java/harness-review/reference.md +0 -86
  118. package/resources/harness/java/harness-run/SKILL.md +0 -148
  119. package/resources/harness/java/harness-run/checklist.md +0 -250
  120. package/resources/harness/java/harness-run/protocols.md +0 -78
  121. package/resources/harness/java/harness-run/reference.md +0 -814
  122. package/resources/harness/java/harness-submit/SKILL.md +0 -166
  123. package/resources/harness/java/harness-submit/checklist.md +0 -407
  124. package/resources/harness/java/harness-submit/reference.md +0 -152
  125. package/resources/harness/java/harness-sync/SKILL.md +0 -82
  126. package/resources/harness/java/harness-sync/reference.md +0 -153
  127. package/resources/harness/java/harness-test/SKILL.md +0 -192
  128. package/resources/harness/java/harness-test/checklist.md +0 -317
  129. package/resources/harness/java/harness-test/pitfalls-java.md +0 -16
  130. package/resources/harness/java/harness-test/pitfalls.md +0 -260
  131. package/resources/harness/java/harness-test/reference.md +0 -788
  132. package/resources/harness/java/protocols/archive-report-protocol.md +0 -175
  133. package/resources/harness/java/protocols/evidence-based-reporting-protocol.md +0 -134
  134. package/resources/harness/java/protocols/ledger-protocol.md +0 -270
  135. package/resources/harness/java/protocols/powershell-protocol.md +0 -181
  136. package/resources/harness/java/protocols/report-pipeline-protocol.md +0 -150
  137. package/resources/harness/java/protocols/sensitive-info-protocol.md +0 -92
  138. package/resources/harness/java/protocols/state-layout-protocol.md +0 -131
  139. package/resources/harness/java/protocols/submit-protocol.md +0 -78
  140. package/resources/harness/java/scripts/harness_archive.py +0 -2319
  141. package/resources/harness/java/scripts/harness_deploy.py +0 -613
  142. package/resources/harness/java/scripts/harness_events.py +0 -680
  143. package/resources/harness/java/scripts/harness_ledger.py +0 -671
  144. package/resources/harness/java/scripts/harness_preflight.py +0 -780
  145. package/resources/harness/java/scripts/harness_service.py +0 -1331
  146. package/resources/harness/manifests/general.json +0 -276
  147. package/resources/harness/manifests/java.json +0 -304
  148. package/resources/harness/migrations/0.1.1/general.json +0 -358
  149. package/resources/harness/migrations/0.1.1/java.json +0 -398
@@ -1,671 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Harness verification-ledger inputsHash fingerprint reuse (D6).
3
-
4
- Subcommands:
5
- hash — compute order-independent inputsHash for a file set
6
- can-reuse — decide reuse / rerun / insufficient-evidence
7
- record — write validation result + inputsHash/inputsFiles into ledger
8
-
9
- Python 3.10+, stdlib only. UTF-8 without BOM. Windows path safe.
10
- """
11
-
12
- from __future__ import annotations
13
-
14
- import argparse
15
- import datetime as dt
16
- import hashlib
17
- import json
18
- import sys
19
- from pathlib import Path
20
- from typing import Any
21
-
22
-
23
- if hasattr(sys.stdout, "reconfigure"):
24
- sys.stdout.reconfigure(encoding="utf-8")
25
- if hasattr(sys.stderr, "reconfigure"):
26
- sys.stderr.reconfigure(encoding="utf-8")
27
-
28
-
29
- VERIFICATIONS = frozenset({"compile", "unitTest", "unitTestFull", "apiTest", "install"})
30
- STATUS_MAP = {
31
- "ok": "OK",
32
- "OK": "OK",
33
- "fail": "FAIL",
34
- "FAIL": "FAIL",
35
- "not_run": "NOT_RUN",
36
- "NOT_RUN": "NOT_RUN",
37
- }
38
- BROAD_SCOPES = frozenset({"module", "module-am", "full"})
39
-
40
-
41
- def now_iso() -> str:
42
- return dt.datetime.now().astimezone().isoformat(timespec="milliseconds")
43
-
44
-
45
- def emit_json(payload: dict[str, Any], *, as_json: bool) -> None:
46
- text = json.dumps(payload, ensure_ascii=False, indent=2) + "\n"
47
- if as_json:
48
- sys.stdout.write(text)
49
- else:
50
- ok = payload.get("ok", True)
51
- reuse = payload.get("reuse")
52
- if reuse is not None:
53
- sys.stdout.write(f"reuse={reuse} reason={payload.get('reason')}\n")
54
- elif "inputsHash" in payload:
55
- sys.stdout.write(f"{payload['inputsHash']}\n")
56
- else:
57
- sys.stdout.write(("ok" if ok else "error") + "\n")
58
-
59
-
60
- def emit_error(message: str, *, as_json: bool, code: int = 1) -> int:
61
- payload = {"ok": False, "error": message}
62
- if as_json:
63
- sys.stderr.write(json.dumps(payload, ensure_ascii=False) + "\n")
64
- else:
65
- sys.stderr.write(f"error: {message}\n")
66
- return code
67
-
68
-
69
- def resolve_path(raw: str) -> Path:
70
- return Path(raw).expanduser().resolve()
71
-
72
-
73
- def parse_files_arg(raw: str | None) -> list[str]:
74
- if raw is None or not str(raw).strip():
75
- return []
76
- parts = [p.strip() for p in str(raw).split(",")]
77
- return [p for p in parts if p]
78
-
79
-
80
- def sha256_file(path: Path) -> str:
81
- digest = hashlib.sha256()
82
- with path.open("rb") as handle:
83
- while True:
84
- chunk = handle.read(1024 * 1024)
85
- if not chunk:
86
- break
87
- digest.update(chunk)
88
- return digest.hexdigest()
89
-
90
-
91
- def compute_inputs_hash(file_paths: list[str]) -> tuple[str, list[str]]:
92
- """Per-file content sha256 → sort digests → hash again (order-independent)."""
93
- by_path: dict[str, str] = {}
94
- for raw in file_paths:
95
- path = resolve_path(raw)
96
- if not path.is_file():
97
- raise FileNotFoundError(f"file not found: {raw}")
98
- by_path[path.as_posix()] = sha256_file(path)
99
-
100
- # Stable file list for callers; bind every resolved path to its digest.
101
- # Hashing only the content multiset let a path swap incorrectly reuse a
102
- # verification result.
103
- resolved_files_sorted = sorted(by_path.keys())
104
-
105
- combined = hashlib.sha256()
106
- for path in resolved_files_sorted:
107
- combined.update(path.encode("utf-8"))
108
- combined.update(b"\0")
109
- combined.update(by_path[path].encode("ascii"))
110
- combined.update(b"\n")
111
- return f"sha256:{combined.hexdigest()}", resolved_files_sorted
112
-
113
-
114
- def expand_profile_input_files(
115
- project: Path, profile_input: str
116
- ) -> tuple[list[str], str | None]:
117
- """Expand verificationInputs[profile_input] globs from build-profile.json.
118
-
119
- Globs are relative to project; only files inside project are kept
120
- (deduped, path-sorted). Returns (files, error); error is None on success.
121
- profile 缺失 / key 缺失 / glob 无匹配 / 结果为空 → 返回 ([], "<reason>"),
122
- 调用方据此返回 insufficient-evidence,执行全量测试但不允许缓存复用。
123
- """
124
- profile_path = project / ".harness" / "config" / "build-profile.json"
125
- if not profile_path.is_file():
126
- return [], "build-profile.json missing; run harness_preflight.py detect"
127
- try:
128
- profile = json.loads(profile_path.read_text(encoding="utf-8-sig"))
129
- except (OSError, json.JSONDecodeError) as exc:
130
- return [], f"build-profile.json unreadable: {exc}"
131
- if not isinstance(profile, dict):
132
- return [], "build-profile.json is not an object"
133
- inputs = profile.get("verificationInputs")
134
- if not isinstance(inputs, dict) or profile_input not in inputs:
135
- return [], f"verificationInputs.{profile_input} missing in build-profile.json"
136
- patterns = inputs[profile_input]
137
- if not isinstance(patterns, list) or not patterns:
138
- return [], f"verificationInputs.{profile_input} is empty or invalid"
139
-
140
- base = project.resolve()
141
- seen: set[str] = set()
142
- for pat in patterns:
143
- if not isinstance(pat, str) or not pat.strip():
144
- continue
145
- for match in base.glob(pat):
146
- if not match.is_file():
147
- continue
148
- resolved = match.resolve()
149
- try:
150
- resolved.relative_to(base)
151
- except ValueError:
152
- # 拒绝 project 外部路径,禁止 glob 逃逸。
153
- continue
154
- seen.add(resolved.as_posix())
155
- if not seen:
156
- return [], f"verificationInputs.{profile_input} matched no files"
157
- return sorted(seen), None
158
-
159
-
160
- def ledger_candidates(change_dir: Path) -> list[Path]:
161
- return [
162
- change_dir / "evidence" / "verification-ledger.json",
163
- change_dir / "verification-ledger.json",
164
- ]
165
-
166
-
167
- def find_ledger_path(change_dir: Path) -> Path | None:
168
- for path in ledger_candidates(change_dir):
169
- if path.is_file():
170
- return path
171
- return None
172
-
173
-
174
- def preferred_write_path(change_dir: Path) -> Path:
175
- # New writes always go to evidence/ (protocol preferred path).
176
- return change_dir / "evidence" / "verification-ledger.json"
177
-
178
-
179
- def load_ledger(change_dir: Path) -> tuple[dict[str, Any] | None, Path | None]:
180
- path = find_ledger_path(change_dir)
181
- if path is None:
182
- return None, None
183
- text = path.read_text(encoding="utf-8-sig")
184
- if not text.strip():
185
- return {}, path
186
- data = json.loads(text)
187
- if data is None:
188
- return {}, path
189
- if not isinstance(data, dict):
190
- raise ValueError(f"ledger must be a JSON object: {path}")
191
- return data, path
192
-
193
-
194
- def write_ledger(path: Path, data: dict[str, Any]) -> None:
195
- path.parent.mkdir(parents=True, exist_ok=True)
196
- payload = json.dumps(data, ensure_ascii=False, indent=2) + "\n"
197
- path.write_text(payload, encoding="utf-8", newline="\n")
198
-
199
-
200
- def normalize_status(raw: str) -> str:
201
- if raw not in STATUS_MAP:
202
- raise ValueError(
203
- f"unsupported status: {raw}; expected one of ok|fail|not_run (case variants OK/FAIL/NOT_RUN)"
204
- )
205
- return STATUS_MAP[raw]
206
-
207
-
208
- def evidence_summary(entry: dict[str, Any]) -> dict[str, Any]:
209
- return {
210
- "status": entry.get("status"),
211
- "command": entry.get("command"),
212
- "evidence": entry.get("evidence"),
213
- "scope": entry.get("scope"),
214
- "inputsHash": entry.get("inputsHash"),
215
- "inputsFiles": entry.get("inputsFiles"),
216
- "durationMs": entry.get("durationMs"),
217
- "exitCode": entry.get("exitCode"),
218
- "finishedAt": entry.get("finishedAt"),
219
- }
220
-
221
-
222
- def _nonempty_str(value: Any) -> bool:
223
- return isinstance(value, str) and bool(value.strip())
224
-
225
-
226
- def _scope_covers(ledger_scope: Any, requested_scope: str | None) -> bool:
227
- """unitTest: ledger scope must cover requested scope (broad scopes cover all)."""
228
- if not _nonempty_str(ledger_scope) and not isinstance(ledger_scope, list):
229
- return False
230
- if requested_scope is None or not str(requested_scope).strip():
231
- # No requested scope → only require ledger to have some scope recorded.
232
- return True
233
-
234
- req = str(requested_scope).strip()
235
- if isinstance(ledger_scope, list):
236
- ledger_items = {str(x).strip() for x in ledger_scope if str(x).strip()}
237
- else:
238
- text = str(ledger_scope).strip()
239
- if text in BROAD_SCOPES:
240
- return True
241
- ledger_items = {p.strip() for p in text.split(",") if p.strip()}
242
-
243
- if req in BROAD_SCOPES:
244
- # Requesting broad scope only reusable if ledger also broad (same or broader).
245
- return str(ledger_scope).strip() in BROAD_SCOPES if not isinstance(ledger_scope, list) else False
246
-
247
- req_items = {p.strip() for p in req.split(",") if p.strip()}
248
- return req_items.issubset(ledger_items)
249
-
250
-
251
- def worktree_ready(ledger: dict[str, Any], change_dir: Path) -> bool:
252
- root = ledger.get("worktreeRoot")
253
- if root is not None and _nonempty_str(root):
254
- return True
255
- meta = change_dir / "meta" / "worktree.json"
256
- if not meta.is_file():
257
- return False
258
- try:
259
- data = json.loads(meta.read_text(encoding="utf-8-sig"))
260
- except (OSError, json.JSONDecodeError):
261
- return False
262
- if not isinstance(data, dict):
263
- return False
264
- if data.get("requested") is True and data.get("created") is True:
265
- return True
266
- if _nonempty_str(data.get("path")) or _nonempty_str(data.get("worktreeRoot")):
267
- return True
268
- return False
269
-
270
-
271
- def decide_can_reuse(
272
- *,
273
- change_dir: Path,
274
- verification: str,
275
- files: list[str],
276
- requested_scope: str | None = None,
277
- requested_command: str | None = None,
278
- ) -> dict[str, Any]:
279
- ledger, ledger_path = load_ledger(change_dir)
280
- if ledger is None:
281
- return {
282
- "ok": True,
283
- "reuse": False,
284
- "reason": "insufficient-evidence",
285
- "verification": verification,
286
- "detail": "ledger missing",
287
- }
288
-
289
- validations = ledger.get("validations")
290
- if not isinstance(validations, dict):
291
- return {
292
- "ok": True,
293
- "reuse": False,
294
- "reason": "insufficient-evidence",
295
- "verification": verification,
296
- "detail": "validations missing",
297
- "ledger_path": str(ledger_path) if ledger_path else None,
298
- }
299
-
300
- entry = validations.get(verification)
301
- if not isinstance(entry, dict):
302
- return {
303
- "ok": True,
304
- "reuse": False,
305
- "reason": "insufficient-evidence",
306
- "verification": verification,
307
- "detail": f"validation '{verification}' missing",
308
- "ledger_path": str(ledger_path) if ledger_path else None,
309
- }
310
-
311
- stored_hash = entry.get("inputsHash")
312
- stored_files = entry.get("inputsFiles")
313
- status = entry.get("status")
314
- evidence = entry.get("evidence")
315
- command = entry.get("command")
316
- scope = entry.get("scope")
317
-
318
- missing: list[str] = []
319
- if not _nonempty_str(stored_hash):
320
- missing.append("inputsHash")
321
- if not isinstance(stored_files, list):
322
- missing.append("inputsFiles")
323
- elif verification == "unitTestFull" and not stored_files:
324
- # 全量门禁的依赖闭包文件集必须非空,禁止空/staged-only 闭包冒充全量。
325
- missing.append("inputsFiles")
326
- if status != "OK":
327
- missing.append("status=OK")
328
- if not _nonempty_str(evidence):
329
- missing.append("evidence")
330
- if not _nonempty_str(command):
331
- missing.append("command")
332
- if verification == "unitTest" and not (
333
- _nonempty_str(scope) or isinstance(scope, list)
334
- ):
335
- missing.append("scope")
336
- if verification == "unitTestFull":
337
- # 独立 full-scope 检查:增量范围(如 FooTest)不能冒充全量门禁。
338
- # 不并入 _scope_covers(),避免依赖增量复用的隐含行为。
339
- if not isinstance(scope, str) or scope.strip() not in BROAD_SCOPES:
340
- missing.append("scope=module|full")
341
- if verification == "install" and not worktree_ready(ledger, change_dir):
342
- missing.append("worktree")
343
-
344
- if missing:
345
- return {
346
- "ok": True,
347
- "reuse": False,
348
- "reason": "insufficient-evidence",
349
- "verification": verification,
350
- "detail": "missing or invalid: " + ", ".join(missing),
351
- "ledger_path": str(ledger_path) if ledger_path else None,
352
- }
353
-
354
- if requested_command is not None and str(requested_command).strip():
355
- if str(command).strip() != str(requested_command).strip():
356
- return {
357
- "ok": True,
358
- "reuse": False,
359
- "reason": "rerun",
360
- "verification": verification,
361
- "detail": "command changed",
362
- "ledger_path": str(ledger_path) if ledger_path else None,
363
- "stored_command": command,
364
- "requested_command": requested_command,
365
- }
366
-
367
- if verification == "unitTest" and not _scope_covers(scope, requested_scope):
368
- return {
369
- "ok": True,
370
- "reuse": False,
371
- "reason": "insufficient-evidence",
372
- "verification": verification,
373
- "detail": "scope does not cover requested tests",
374
- "ledger_path": str(ledger_path) if ledger_path else None,
375
- "stored_scope": scope,
376
- "requested_scope": requested_scope,
377
- }
378
-
379
- try:
380
- current_hash, current_files = compute_inputs_hash(files)
381
- except FileNotFoundError as exc:
382
- return {
383
- "ok": True,
384
- "reuse": False,
385
- "reason": "insufficient-evidence",
386
- "verification": verification,
387
- "detail": str(exc),
388
- "ledger_path": str(ledger_path) if ledger_path else None,
389
- }
390
-
391
- if current_hash != stored_hash:
392
- return {
393
- "ok": True,
394
- "reuse": False,
395
- "reason": "rerun",
396
- "verification": verification,
397
- "detail": "inputsHash changed",
398
- "ledger_path": str(ledger_path) if ledger_path else None,
399
- "stored_inputsHash": stored_hash,
400
- "current_inputsHash": current_hash,
401
- "inputsFiles": current_files,
402
- }
403
-
404
- return {
405
- "ok": True,
406
- "reuse": True,
407
- "reason": "reuse",
408
- "verification": verification,
409
- "ledger_path": str(ledger_path) if ledger_path else None,
410
- "inputsHash": stored_hash,
411
- "inputsFiles": stored_files,
412
- "evidence_summary": evidence_summary(entry),
413
- "marker": "REUSED",
414
- }
415
-
416
-
417
- def cmd_hash(args: argparse.Namespace) -> int:
418
- as_json = bool(args.json)
419
- files = parse_files_arg(args.files)
420
- try:
421
- inputs_hash, inputs_files = compute_inputs_hash(files)
422
- except (OSError, FileNotFoundError) as exc:
423
- return emit_error(str(exc), as_json=as_json)
424
-
425
- payload = {
426
- "ok": True,
427
- "action": "hash",
428
- "inputsHash": inputs_hash,
429
- "inputsFiles": inputs_files,
430
- "fileCount": len(inputs_files),
431
- }
432
- emit_json(payload, as_json=as_json)
433
- return 0
434
-
435
-
436
- def cmd_can_reuse(args: argparse.Namespace) -> int:
437
- as_json = bool(args.json)
438
- verification = args.verification
439
- if verification not in VERIFICATIONS:
440
- return emit_error(
441
- f"unsupported verification: {verification}; expected one of {sorted(VERIFICATIONS)}",
442
- as_json=as_json,
443
- )
444
- change_dir = resolve_path(args.change_dir)
445
- files = parse_files_arg(args.files)
446
- profile_input = getattr(args, "profile_input", None)
447
- project_raw = getattr(args, "project", None)
448
- if profile_input:
449
- if not project_raw:
450
- return emit_error("--profile-input requires --project", as_json=as_json)
451
- resolved_files, err = expand_profile_input_files(
452
- Path(str(project_raw)).expanduser().resolve(), profile_input
453
- )
454
- if err:
455
- # profile 未正确配置:不允许缓存复用,返回 insufficient-evidence(exit 0)。
456
- payload = {
457
- "ok": True,
458
- "reuse": False,
459
- "reason": "insufficient-evidence",
460
- "verification": verification,
461
- "detail": err,
462
- }
463
- emit_json(payload, as_json=as_json)
464
- return 0
465
- files = resolved_files
466
- if not files:
467
- return emit_error(
468
- "can-reuse requires --files or a non-empty --profile-input file set",
469
- as_json=as_json,
470
- )
471
- try:
472
- payload = decide_can_reuse(
473
- change_dir=change_dir,
474
- verification=verification,
475
- files=files,
476
- requested_scope=getattr(args, "scope", None),
477
- requested_command=getattr(args, "command", None),
478
- )
479
- except (OSError, ValueError, json.JSONDecodeError) as exc:
480
- return emit_error(f"can-reuse failed: {exc}", as_json=as_json)
481
-
482
- emit_json(payload, as_json=as_json)
483
- return 0
484
-
485
-
486
- def cmd_record(args: argparse.Namespace) -> int:
487
- as_json = bool(args.json)
488
- change_dir = resolve_path(args.change_dir)
489
- verification = args.verification
490
- files = parse_files_arg(args.files)
491
- profile_input = getattr(args, "profile_input", None)
492
- project_raw = getattr(args, "project", None)
493
- if profile_input:
494
- if not project_raw:
495
- return emit_error("--profile-input requires --project", as_json=as_json)
496
- resolved_files, err = expand_profile_input_files(
497
- Path(str(project_raw)).expanduser().resolve(), profile_input
498
- )
499
- if err:
500
- return emit_error(f"record failed: {err}", as_json=as_json)
501
- files = resolved_files
502
- if not files:
503
- return emit_error(
504
- "record requires --files or a non-empty --profile-input file set",
505
- as_json=as_json,
506
- )
507
-
508
- try:
509
- status = normalize_status(args.status)
510
- inputs_hash, inputs_files = compute_inputs_hash(files)
511
- ledger, _existing_path = load_ledger(change_dir)
512
- if ledger is None:
513
- ledger = {
514
- "changeName": change_dir.name,
515
- "stateDir": str(change_dir),
516
- "validations": {},
517
- }
518
- elif not isinstance(ledger.get("validations"), dict):
519
- ledger["validations"] = {}
520
-
521
- # Preserve top-level diffHash and all other existing fields (backward compatible).
522
- entry = {}
523
- prev = ledger["validations"].get(verification)
524
- if isinstance(prev, dict):
525
- entry.update(prev)
526
-
527
- entry.update(
528
- {
529
- "status": status,
530
- "command": args.command,
531
- "evidence": args.evidence,
532
- "exitCode": args.exit_code,
533
- "durationMs": args.duration_ms,
534
- "inputsHash": inputs_hash,
535
- "inputsFiles": inputs_files,
536
- "finishedAt": now_iso(),
537
- }
538
- )
539
- if args.scope is not None and str(args.scope).strip():
540
- entry["scope"] = str(args.scope).strip()
541
- # No default scope: recording an incremental run as broad "module" scope
542
- # would let can-reuse wrongly approve untested classes (D13 guardrail).
543
- # Missing scope → can-reuse treats unitTest as insufficient-evidence.
544
-
545
- ledger["validations"][verification] = entry
546
- if "changeName" not in ledger:
547
- ledger["changeName"] = change_dir.name
548
- if "stateDir" not in ledger:
549
- ledger["stateDir"] = str(change_dir)
550
-
551
- out_path = preferred_write_path(change_dir)
552
- write_ledger(out_path, ledger)
553
- except (OSError, ValueError, FileNotFoundError, json.JSONDecodeError) as exc:
554
- return emit_error(f"record failed: {exc}", as_json=as_json)
555
-
556
- payload = {
557
- "ok": True,
558
- "action": "record",
559
- "verification": verification,
560
- "status": status,
561
- "inputsHash": inputs_hash,
562
- "inputsFiles": inputs_files,
563
- "ledger_path": str(out_path),
564
- "diffHash": ledger.get("diffHash"),
565
- }
566
- emit_json(payload, as_json=as_json)
567
- return 0
568
-
569
-
570
- def build_parser() -> argparse.ArgumentParser:
571
- parser = argparse.ArgumentParser(
572
- prog="harness_ledger.py",
573
- description="Compute inputsHash and manage verification-ledger reuse",
574
- )
575
- parser.add_argument(
576
- "--json",
577
- action="store_true",
578
- help="emit machine-readable JSON on stdout",
579
- )
580
- sub = parser.add_subparsers(dest="command_name", required=True)
581
-
582
- # --json 也注册到每个子命令(default=SUPPRESS),使 --json 可放在子命令之后
583
- # (skill / Gate 命令均把 --json 放最后),且不会用子命令默认值覆盖
584
- # 在子命令之前传入的顶层 --json=True。
585
- shared_json = argparse.ArgumentParser(add_help=False)
586
- shared_json.add_argument(
587
- "--json",
588
- action="store_true",
589
- default=argparse.SUPPRESS,
590
- )
591
-
592
- p_hash = sub.add_parser("hash", parents=[shared_json], help="compute inputsHash for a file set")
593
- p_hash.add_argument(
594
- "--files",
595
- required=True,
596
- help="comma-separated source file paths",
597
- )
598
- p_hash.set_defaults(func=cmd_hash)
599
-
600
- p_reuse = sub.add_parser("can-reuse", parents=[shared_json], help="decide whether a verification can be reused")
601
- p_reuse.add_argument("--change-dir", required=True)
602
- p_reuse.add_argument(
603
- "--verification",
604
- required=True,
605
- choices=sorted(VERIFICATIONS),
606
- )
607
- p_reuse.add_argument(
608
- "--files",
609
- default=None,
610
- help="comma-separated source file paths for current inputsHash",
611
- )
612
- p_reuse.add_argument(
613
- "--project",
614
- default=None,
615
- help="project root containing .harness/config/build-profile.json (for --profile-input)",
616
- )
617
- p_reuse.add_argument(
618
- "--profile-input",
619
- default=None,
620
- help="expand verificationInputs.<key> globs from build-profile as the file set; "
621
- "unitTestFull 最终门禁用此展开依赖闭包,禁止用仅含 staged 文件的 --files 冒充",
622
- )
623
- p_reuse.add_argument(
624
- "--scope",
625
- default=None,
626
- help="optional requested scope (unitTest coverage check)",
627
- )
628
- p_reuse.add_argument(
629
- "--command",
630
- default=None,
631
- help="optional command to compare against ledger entry",
632
- )
633
- p_reuse.set_defaults(func=cmd_can_reuse)
634
-
635
- p_record = sub.add_parser("record", parents=[shared_json], help="write validation result into ledger")
636
- p_record.add_argument("--change-dir", required=True)
637
- p_record.add_argument("--verification", required=True)
638
- p_record.add_argument("--status", required=True)
639
- p_record.add_argument("--command", required=True)
640
- p_record.add_argument("--exit-code", type=int, required=True)
641
- p_record.add_argument("--duration-ms", type=int, required=True)
642
- p_record.add_argument("--files", default=None)
643
- p_record.add_argument("--evidence", required=True)
644
- p_record.add_argument(
645
- "--project",
646
- default=None,
647
- help="project root containing .harness/config/build-profile.json (for --profile-input)",
648
- )
649
- p_record.add_argument(
650
- "--profile-input",
651
- default=None,
652
- help="expand verificationInputs.<key> globs from build-profile as the file set",
653
- )
654
- p_record.add_argument(
655
- "--scope",
656
- default=None,
657
- help="optional scope (default module when absent on new entries)",
658
- )
659
- p_record.set_defaults(func=cmd_record)
660
-
661
- return parser
662
-
663
-
664
- def main(argv: list[str] | None = None) -> int:
665
- parser = build_parser()
666
- args = parser.parse_args(argv)
667
- return int(args.func(args))
668
-
669
-
670
- if __name__ == "__main__":
671
- raise SystemExit(main())