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.
- package/LICENSE +21 -21
- package/README.md +11 -0
- package/dist/bin.js +1540 -491
- package/package.json +9 -7
- package/resources/harness/general/.harness-build.json +0 -5
- package/resources/harness/general/CONTEXT.md +0 -65
- package/resources/harness/general/README.md +0 -478
- package/resources/harness/general/agents/harness-evaluator.md +0 -96
- package/resources/harness/general/agents/harness-explorer.md +0 -72
- package/resources/harness/general/agents/harness-reviewer.md +0 -69
- package/resources/harness/general/harness-archive/SKILL.md +0 -215
- package/resources/harness/general/harness-archive/checklist.md +0 -52
- package/resources/harness/general/harness-archive/reference.md +0 -103
- package/resources/harness/general/harness-archive/scripts/gen-manifest.ps1 +0 -31
- package/resources/harness/general/harness-archive/templates/render-summary.mjs +0 -171
- package/resources/harness/general/harness-archive/templates/summary-data-template.json +0 -128
- package/resources/harness/general/harness-codebase-map/SKILL.md +0 -112
- package/resources/harness/general/harness-codebase-map/checklist.md +0 -126
- package/resources/harness/general/harness-codebase-map/reference.md +0 -364
- package/resources/harness/general/harness-codebase-map/templates/ARCHITECTURE.md +0 -57
- package/resources/harness/general/harness-codebase-map/templates/CONCERNS.md +0 -49
- package/resources/harness/general/harness-codebase-map/templates/CONVENTIONS.md +0 -57
- package/resources/harness/general/harness-codebase-map/templates/INTEGRATIONS.md +0 -52
- package/resources/harness/general/harness-codebase-map/templates/STACK.md +0 -55
- package/resources/harness/general/harness-codebase-map/templates/STRUCTURE.md +0 -69
- package/resources/harness/general/harness-codebase-map/templates/TESTING.md +0 -64
- package/resources/harness/general/harness-codebase-map/templates/map-manifest.schema.json +0 -73
- package/resources/harness/general/harness-codebase-map/templates/map-summary.md +0 -64
- package/resources/harness/general/harness-knowledge-ingest/SKILL.md +0 -246
- package/resources/harness/general/harness-knowledge-ingest/design.md +0 -842
- package/resources/harness/general/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -42
- package/resources/harness/general/harness-knowledge-ingest/mcp-config.example.json +0 -10
- package/resources/harness/general/harness-knowledge-ingest/reference.md +0 -309
- package/resources/harness/general/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -4082
- package/resources/harness/general/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -273
- package/resources/harness/general/harness-knowledge-query/SKILL.md +0 -164
- package/resources/harness/general/harness-plan/SKILL.md +0 -127
- package/resources/harness/general/harness-plan/checklist.md +0 -243
- package/resources/harness/general/harness-plan/protocols.md +0 -124
- package/resources/harness/general/harness-plan/reference.md +0 -376
- package/resources/harness/general/harness-review/SKILL.md +0 -156
- package/resources/harness/general/harness-review/checklist.md +0 -124
- package/resources/harness/general/harness-review/protocols.md +0 -68
- package/resources/harness/general/harness-review/reference.md +0 -86
- package/resources/harness/general/harness-run/SKILL.md +0 -132
- package/resources/harness/general/harness-run/checklist.md +0 -259
- package/resources/harness/general/harness-run/protocols.md +0 -78
- package/resources/harness/general/harness-run/reference.md +0 -834
- package/resources/harness/general/harness-submit/SKILL.md +0 -159
- package/resources/harness/general/harness-submit/checklist.md +0 -407
- package/resources/harness/general/harness-submit/reference.md +0 -152
- package/resources/harness/general/harness-sync/SKILL.md +0 -82
- package/resources/harness/general/harness-sync/reference.md +0 -153
- package/resources/harness/general/harness-test/SKILL.md +0 -180
- package/resources/harness/general/harness-test/checklist.md +0 -319
- package/resources/harness/general/harness-test/pitfalls.md +0 -260
- package/resources/harness/general/harness-test/reference.md +0 -791
- package/resources/harness/general/protocols/archive-report-protocol.md +0 -175
- package/resources/harness/general/protocols/evidence-based-reporting-protocol.md +0 -134
- package/resources/harness/general/protocols/ledger-protocol.md +0 -270
- package/resources/harness/general/protocols/powershell-protocol.md +0 -181
- package/resources/harness/general/protocols/report-pipeline-protocol.md +0 -150
- package/resources/harness/general/protocols/sensitive-info-protocol.md +0 -92
- package/resources/harness/general/protocols/state-layout-protocol.md +0 -131
- package/resources/harness/general/protocols/submit-protocol.md +0 -78
- package/resources/harness/general/scripts/harness_archive.py +0 -2319
- package/resources/harness/general/scripts/harness_deploy.py +0 -613
- package/resources/harness/general/scripts/harness_events.py +0 -680
- package/resources/harness/general/scripts/harness_ledger.py +0 -671
- package/resources/harness/general/scripts/harness_preflight.py +0 -780
- package/resources/harness/general/scripts/harness_service.py +0 -1331
- package/resources/harness/java/.harness-build.json +0 -5
- package/resources/harness/java/CONTEXT.md +0 -65
- package/resources/harness/java/README.md +0 -478
- package/resources/harness/java/agents/harness-evaluator.md +0 -96
- package/resources/harness/java/agents/harness-explorer.md +0 -72
- package/resources/harness/java/agents/harness-reviewer.md +0 -69
- package/resources/harness/java/harness-apidoc/SKILL.md +0 -86
- package/resources/harness/java/harness-apidoc/checklist.md +0 -142
- package/resources/harness/java/harness-apidoc/reference.md +0 -233
- package/resources/harness/java/harness-archive/SKILL.md +0 -215
- package/resources/harness/java/harness-archive/checklist.md +0 -52
- package/resources/harness/java/harness-archive/reference.md +0 -103
- package/resources/harness/java/harness-archive/scripts/gen-manifest.ps1 +0 -31
- package/resources/harness/java/harness-archive/templates/render-summary.mjs +0 -171
- package/resources/harness/java/harness-archive/templates/summary-data-template.json +0 -128
- package/resources/harness/java/harness-codebase-map/SKILL.md +0 -112
- package/resources/harness/java/harness-codebase-map/checklist.md +0 -126
- package/resources/harness/java/harness-codebase-map/reference.md +0 -364
- package/resources/harness/java/harness-codebase-map/templates/ARCHITECTURE.md +0 -57
- package/resources/harness/java/harness-codebase-map/templates/CONCERNS.md +0 -49
- package/resources/harness/java/harness-codebase-map/templates/CONVENTIONS.md +0 -57
- package/resources/harness/java/harness-codebase-map/templates/INTEGRATIONS.md +0 -52
- package/resources/harness/java/harness-codebase-map/templates/STACK.md +0 -55
- package/resources/harness/java/harness-codebase-map/templates/STRUCTURE.md +0 -69
- package/resources/harness/java/harness-codebase-map/templates/TESTING.md +0 -64
- package/resources/harness/java/harness-codebase-map/templates/map-manifest.schema.json +0 -73
- package/resources/harness/java/harness-codebase-map/templates/map-summary.md +0 -64
- package/resources/harness/java/harness-knowledge-ingest/SKILL.md +0 -246
- package/resources/harness/java/harness-knowledge-ingest/design.md +0 -842
- package/resources/harness/java/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -42
- package/resources/harness/java/harness-knowledge-ingest/mcp-config.example.json +0 -10
- package/resources/harness/java/harness-knowledge-ingest/reference.md +0 -309
- package/resources/harness/java/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -4082
- package/resources/harness/java/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -273
- package/resources/harness/java/harness-knowledge-query/SKILL.md +0 -164
- package/resources/harness/java/harness-package/SKILL.md +0 -87
- package/resources/harness/java/harness-package/checklist.md +0 -322
- package/resources/harness/java/harness-package/reference.md +0 -210
- package/resources/harness/java/harness-plan/SKILL.md +0 -127
- package/resources/harness/java/harness-plan/checklist.md +0 -243
- package/resources/harness/java/harness-plan/protocols.md +0 -124
- package/resources/harness/java/harness-plan/reference.md +0 -376
- package/resources/harness/java/harness-review/SKILL.md +0 -156
- package/resources/harness/java/harness-review/checklist.md +0 -124
- package/resources/harness/java/harness-review/protocols.md +0 -68
- package/resources/harness/java/harness-review/reference.md +0 -86
- package/resources/harness/java/harness-run/SKILL.md +0 -148
- package/resources/harness/java/harness-run/checklist.md +0 -250
- package/resources/harness/java/harness-run/protocols.md +0 -78
- package/resources/harness/java/harness-run/reference.md +0 -814
- package/resources/harness/java/harness-submit/SKILL.md +0 -166
- package/resources/harness/java/harness-submit/checklist.md +0 -407
- package/resources/harness/java/harness-submit/reference.md +0 -152
- package/resources/harness/java/harness-sync/SKILL.md +0 -82
- package/resources/harness/java/harness-sync/reference.md +0 -153
- package/resources/harness/java/harness-test/SKILL.md +0 -192
- package/resources/harness/java/harness-test/checklist.md +0 -317
- package/resources/harness/java/harness-test/pitfalls-java.md +0 -16
- package/resources/harness/java/harness-test/pitfalls.md +0 -260
- package/resources/harness/java/harness-test/reference.md +0 -788
- package/resources/harness/java/protocols/archive-report-protocol.md +0 -175
- package/resources/harness/java/protocols/evidence-based-reporting-protocol.md +0 -134
- package/resources/harness/java/protocols/ledger-protocol.md +0 -270
- package/resources/harness/java/protocols/powershell-protocol.md +0 -181
- package/resources/harness/java/protocols/report-pipeline-protocol.md +0 -150
- package/resources/harness/java/protocols/sensitive-info-protocol.md +0 -92
- package/resources/harness/java/protocols/state-layout-protocol.md +0 -131
- package/resources/harness/java/protocols/submit-protocol.md +0 -78
- package/resources/harness/java/scripts/harness_archive.py +0 -2319
- package/resources/harness/java/scripts/harness_deploy.py +0 -613
- package/resources/harness/java/scripts/harness_events.py +0 -680
- package/resources/harness/java/scripts/harness_ledger.py +0 -671
- package/resources/harness/java/scripts/harness_preflight.py +0 -780
- package/resources/harness/java/scripts/harness_service.py +0 -1331
- package/resources/harness/manifests/general.json +0 -276
- package/resources/harness/manifests/java.json +0 -304
- package/resources/harness/migrations/0.1.1/general.json +0 -358
- 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())
|