@hunter-harness/workflow-harness 0.2.78 → 0.2.79
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/harness/bundles/general/claude-code/.harness-build.json +1 -1
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +17 -2
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-push/SKILL.md +16 -6
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/protocols/sensitive-info-protocol.md +5 -0
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +404 -46
- package/harness/bundles/general/claude-code/scripts/harness_runtime.py +6 -2
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +17 -2
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-push/SKILL.md +15 -5
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/protocols/sensitive-info-protocol.md +5 -0
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +404 -46
- package/harness/bundles/general/codebuddy/scripts/harness_runtime.py +6 -2
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +17 -2
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-push/SKILL.md +15 -5
- package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codex/protocols/sensitive-info-protocol.md +5 -0
- package/harness/bundles/general/codex/scripts/harness_archive.py +404 -46
- package/harness/bundles/general/codex/scripts/harness_runtime.py +6 -2
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +17 -2
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-push/SKILL.md +15 -5
- package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/cursor/protocols/sensitive-info-protocol.md +5 -0
- package/harness/bundles/general/cursor/scripts/harness_archive.py +404 -46
- package/harness/bundles/general/cursor/scripts/harness_runtime.py +6 -2
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +17 -2
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-push/SKILL.md +16 -6
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/protocols/sensitive-info-protocol.md +5 -0
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +404 -46
- package/harness/bundles/java/claude-code/scripts/harness_runtime.py +6 -2
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +17 -2
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-push/SKILL.md +15 -5
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/protocols/sensitive-info-protocol.md +5 -0
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +404 -46
- package/harness/bundles/java/codebuddy/scripts/harness_runtime.py +6 -2
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +17 -2
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-push/SKILL.md +15 -5
- package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codex/protocols/sensitive-info-protocol.md +5 -0
- package/harness/bundles/java/codex/scripts/harness_archive.py +404 -46
- package/harness/bundles/java/codex/scripts/harness_runtime.py +6 -2
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +17 -2
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-push/SKILL.md +15 -5
- package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/cursor/protocols/sensitive-info-protocol.md +5 -0
- package/harness/bundles/java/cursor/scripts/harness_archive.py +404 -46
- package/harness/bundles/java/cursor/scripts/harness_runtime.py +6 -2
- package/harness/manifests/general/claude-code.json +18 -18
- package/harness/manifests/general/codebuddy.json +18 -18
- package/harness/manifests/general/codex.json +18 -18
- package/harness/manifests/general/cursor.json +18 -18
- package/harness/manifests/java/claude-code.json +20 -20
- package/harness/manifests/java/codebuddy.json +20 -20
- package/harness/manifests/java/codex.json +20 -20
- package/harness/manifests/java/cursor.json +20 -20
- package/hunter-workflow-family.json +5 -5
- package/package.json +1 -1
|
@@ -259,9 +259,13 @@ def append_event(
|
|
|
259
259
|
def generate_manifest(root: Path, output_path: Path) -> dict[str, Any]:
|
|
260
260
|
"""Build path/size/sha256 manifest; exclude the output file itself."""
|
|
261
261
|
root = root.resolve()
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
262
|
+
# Compare by relative path, not by resolve() per file: on Windows every
|
|
263
|
+
# resolve() is a realpath syscall, and this loop runs over the whole tree.
|
|
264
|
+
exclude_rel: str | None = None
|
|
265
|
+
try:
|
|
266
|
+
exclude_rel = output_path.resolve().relative_to(root).as_posix()
|
|
267
|
+
except (OSError, ValueError):
|
|
268
|
+
exclude_rel = None
|
|
265
269
|
|
|
266
270
|
files: list[dict[str, Any]] = []
|
|
267
271
|
total_bytes = 0
|
|
@@ -270,9 +274,9 @@ def generate_manifest(root: Path, output_path: Path) -> dict[str, Any]:
|
|
|
270
274
|
continue
|
|
271
275
|
if path.name.endswith(".lock"):
|
|
272
276
|
continue
|
|
273
|
-
if exclude is not None and path.resolve() == exclude:
|
|
274
|
-
continue
|
|
275
277
|
rel = path.relative_to(root).as_posix()
|
|
278
|
+
if exclude_rel is not None and rel == exclude_rel:
|
|
279
|
+
continue
|
|
276
280
|
size = path.stat().st_size
|
|
277
281
|
total_bytes += size
|
|
278
282
|
files.append(
|
|
@@ -2113,6 +2117,56 @@ def check_archive_exact_byte_policy(project_root: Path) -> dict[str, Any]:
|
|
|
2113
2117
|
}
|
|
2114
2118
|
|
|
2115
2119
|
|
|
2120
|
+
SENSITIVE_SCAN_POLICY_ENV = "HUNTER_HARNESS_SENSITIVE_SCAN"
|
|
2121
|
+
DEFAULT_SENSITIVE_SCAN_POLICY = "warn"
|
|
2122
|
+
|
|
2123
|
+
|
|
2124
|
+
def resolve_sensitive_scan_policy(env: dict[str, str] | None = None) -> str:
|
|
2125
|
+
"""Publication-gate policy: ``off`` | ``warn`` | ``block`` (default warn).
|
|
2126
|
+
|
|
2127
|
+
Mirrors resolveSensitiveScanPolicy() in packages/core so the archive gate
|
|
2128
|
+
and the push gate answer to the same switch.
|
|
2129
|
+
"""
|
|
2130
|
+
source = os.environ if env is None else env
|
|
2131
|
+
raw = str(source.get(SENSITIVE_SCAN_POLICY_ENV) or "").strip().lower()
|
|
2132
|
+
if raw in {"off", "false", "0", "disabled"}:
|
|
2133
|
+
return "off"
|
|
2134
|
+
if raw in {"warn", "advisory"}:
|
|
2135
|
+
return "warn"
|
|
2136
|
+
if raw in {"block", "true", "1", "enforce"}:
|
|
2137
|
+
return "block"
|
|
2138
|
+
return DEFAULT_SENSITIVE_SCAN_POLICY
|
|
2139
|
+
|
|
2140
|
+
|
|
2141
|
+
def _advisory_sensitive_gate(result: dict[str, Any], policy: str) -> dict[str, Any]:
|
|
2142
|
+
"""Downgrade a failed gate to advisory under a non-blocking policy.
|
|
2143
|
+
|
|
2144
|
+
The facts survive verbatim under ``advisory`` so the archive report still
|
|
2145
|
+
names every plaintext file; only the veto is dropped. A fail-closed gate
|
|
2146
|
+
with no proportionate setting gets worked around rather than respected.
|
|
2147
|
+
"""
|
|
2148
|
+
if result.get("ok") or policy == "block":
|
|
2149
|
+
return result
|
|
2150
|
+
return {
|
|
2151
|
+
"ok": True,
|
|
2152
|
+
"reasonCode": "SENSITIVE_EVIDENCE_ADVISORY",
|
|
2153
|
+
"policy": policy,
|
|
2154
|
+
"receiptPath": result.get("receiptPath"),
|
|
2155
|
+
"unresolvedFailures": [],
|
|
2156
|
+
"advisory": {
|
|
2157
|
+
key: value
|
|
2158
|
+
for key, value in result.items()
|
|
2159
|
+
if key not in {"ok", "policy"}
|
|
2160
|
+
},
|
|
2161
|
+
"nextAction": (
|
|
2162
|
+
"Findings are advisory under "
|
|
2163
|
+
f"{SENSITIVE_SCAN_POLICY_ENV}={policy}; set it to `block` to veto "
|
|
2164
|
+
"publication, or quarantine with `harness_runtime.py "
|
|
2165
|
+
"quarantine-evidence`."
|
|
2166
|
+
),
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
|
|
2116
2170
|
def validate_sensitive_evidence_publication_gate(
|
|
2117
2171
|
change_dir: Path,
|
|
2118
2172
|
*,
|
|
@@ -2129,24 +2183,42 @@ def validate_sensitive_evidence_publication_gate(
|
|
|
2129
2183
|
"""
|
|
2130
2184
|
change_dir = change_dir.expanduser().resolve()
|
|
2131
2185
|
copy_root = (copy_root or change_dir).expanduser().resolve()
|
|
2186
|
+
policy = resolve_sensitive_scan_policy()
|
|
2132
2187
|
receipt_path = hruntime.sensitive_evidence_receipt_path(change_dir)
|
|
2133
|
-
|
|
2134
|
-
|
|
2188
|
+
if policy == "off":
|
|
2189
|
+
# Skipping the scan also skips a full-tree byte read of the change dir.
|
|
2135
2190
|
return {
|
|
2136
|
-
"ok":
|
|
2137
|
-
"reasonCode": "
|
|
2191
|
+
"ok": True,
|
|
2192
|
+
"reasonCode": "SECRET_SCAN_DISABLED",
|
|
2193
|
+
"policy": policy,
|
|
2138
2194
|
"receiptPath": str(receipt_path),
|
|
2139
|
-
"unresolvedFailures":
|
|
2140
|
-
"nextAction": "Quarantine the original bytes before archive publication.",
|
|
2195
|
+
"unresolvedFailures": [],
|
|
2141
2196
|
}
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2197
|
+
candidates = hruntime.sensitive_evidence_candidates(change_dir)
|
|
2198
|
+
if candidates:
|
|
2199
|
+
return _advisory_sensitive_gate(
|
|
2200
|
+
{
|
|
2145
2201
|
"ok": False,
|
|
2146
|
-
"reasonCode": "
|
|
2202
|
+
"reasonCode": "SENSITIVE_EVIDENCE_UNQUARANTINED",
|
|
2147
2203
|
"receiptPath": str(receipt_path),
|
|
2148
|
-
"unresolvedFailures":
|
|
2149
|
-
|
|
2204
|
+
"unresolvedFailures": candidates,
|
|
2205
|
+
"nextAction": (
|
|
2206
|
+
"Quarantine the original bytes before archive publication."
|
|
2207
|
+
),
|
|
2208
|
+
},
|
|
2209
|
+
policy,
|
|
2210
|
+
)
|
|
2211
|
+
if receipt_override is None and not receipt_path.is_file():
|
|
2212
|
+
if require_receipt:
|
|
2213
|
+
return _advisory_sensitive_gate(
|
|
2214
|
+
{
|
|
2215
|
+
"ok": False,
|
|
2216
|
+
"reasonCode": "SECRET_SCAN_RECEIPT_MISSING",
|
|
2217
|
+
"receiptPath": str(receipt_path),
|
|
2218
|
+
"unresolvedFailures": [],
|
|
2219
|
+
},
|
|
2220
|
+
policy,
|
|
2221
|
+
)
|
|
2150
2222
|
return {
|
|
2151
2223
|
"ok": True,
|
|
2152
2224
|
"reasonCode": "SECRET_SCAN_NOT_APPLICABLE",
|
|
@@ -2158,21 +2230,27 @@ def validate_sensitive_evidence_publication_gate(
|
|
|
2158
2230
|
try:
|
|
2159
2231
|
receipt = read_json(receipt_path)
|
|
2160
2232
|
except (OSError, json.JSONDecodeError, TypeError) as exc:
|
|
2161
|
-
return
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2233
|
+
return _advisory_sensitive_gate(
|
|
2234
|
+
{
|
|
2235
|
+
"ok": False,
|
|
2236
|
+
"reasonCode": "SECRET_SCAN_RECEIPT_INVALID",
|
|
2237
|
+
"receiptPath": str(receipt_path),
|
|
2238
|
+
"error": str(exc),
|
|
2239
|
+
},
|
|
2240
|
+
policy,
|
|
2241
|
+
)
|
|
2167
2242
|
else:
|
|
2168
2243
|
receipt = receipt_override
|
|
2169
2244
|
if not isinstance(receipt, dict):
|
|
2170
|
-
return
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2245
|
+
return _advisory_sensitive_gate(
|
|
2246
|
+
{
|
|
2247
|
+
"ok": False,
|
|
2248
|
+
"reasonCode": "SECRET_SCAN_RECEIPT_INVALID",
|
|
2249
|
+
"receiptPath": str(receipt_path),
|
|
2250
|
+
"error": "receipt must be an object",
|
|
2251
|
+
},
|
|
2252
|
+
policy,
|
|
2253
|
+
)
|
|
2176
2254
|
issues: list[dict[str, Any]] = []
|
|
2177
2255
|
if receipt.get("schemaVersion") != 1:
|
|
2178
2256
|
issues.append({"code": "SECRET_SCAN_SCHEMA_INVALID"})
|
|
@@ -2232,14 +2310,19 @@ def validate_sensitive_evidence_publication_gate(
|
|
|
2232
2310
|
"code": "SENSITIVE_EVIDENCE_SOURCE_REMAINS",
|
|
2233
2311
|
"sourcePath": source_raw,
|
|
2234
2312
|
})
|
|
2235
|
-
return
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2313
|
+
return _advisory_sensitive_gate(
|
|
2314
|
+
{
|
|
2315
|
+
"ok": not issues,
|
|
2316
|
+
"reasonCode": (
|
|
2317
|
+
"SECRET_SCAN_GATE_SATISFIED" if not issues else "SECRET_SCAN_GATE_BLOCKED"
|
|
2318
|
+
),
|
|
2319
|
+
"receiptPath": str(receipt_path),
|
|
2320
|
+
"receipt": receipt,
|
|
2321
|
+
"issues": issues,
|
|
2322
|
+
"treeDigest": actual_digest,
|
|
2323
|
+
},
|
|
2324
|
+
policy,
|
|
2325
|
+
)
|
|
2243
2326
|
|
|
2244
2327
|
|
|
2245
2328
|
# ---------------------------------------------------------------------------
|
|
@@ -8339,14 +8422,16 @@ def cmd_finalize(
|
|
|
8339
8422
|
if before_in_archive.is_file():
|
|
8340
8423
|
before_manifest = read_json(before_in_archive)
|
|
8341
8424
|
compare_result = compare_manifests(before_manifest, after_manifest)
|
|
8342
|
-
coverage = verify_manifest_byte_coverage(work_dir, after_manifest)
|
|
8343
8425
|
# Embed compare stats into summary AFTER manifest → must exclude those paths.
|
|
8426
|
+
# Only one coverage pass runs: an earlier build verified here as well,
|
|
8427
|
+
# but that result was overwritten a few lines below by the pass that
|
|
8428
|
+
# excludes summary-data.json, so it only cost a full extra tree hash.
|
|
8344
8429
|
summary = read_json(summary_path)
|
|
8345
8430
|
summary["archiveManifest"] = {
|
|
8346
8431
|
"movedFiles": compare_result.get("movedFiles", 0),
|
|
8347
8432
|
"generatedFiles": compare_result.get("generatedFiles", 0),
|
|
8348
8433
|
"totalArchiveFiles": compare_result.get("totalArchiveFiles", 0),
|
|
8349
|
-
"checksumStatus":
|
|
8434
|
+
"checksumStatus": "PENDING",
|
|
8350
8435
|
"exclusionReasons": {
|
|
8351
8436
|
"reports/final/summary-data.json": (
|
|
8352
8437
|
"archiveManifest stats written after coverage snapshot"
|
|
@@ -8620,6 +8705,7 @@ def cmd_finalize(
|
|
|
8620
8705
|
"archiveId",
|
|
8621
8706
|
"archiveStatus",
|
|
8622
8707
|
"knowledgeStatus",
|
|
8708
|
+
"knowledgeCandidateCount",
|
|
8623
8709
|
"uploadStatus",
|
|
8624
8710
|
"packageSha256",
|
|
8625
8711
|
"manifestSha256",
|
|
@@ -9053,14 +9139,50 @@ def resolve_npx_launcher() -> list[str]:
|
|
|
9053
9139
|
return [node, str(npx_cli)]
|
|
9054
9140
|
|
|
9055
9141
|
|
|
9142
|
+
def _installed_cli_entry(project_root: Path) -> Path | None:
|
|
9143
|
+
"""Find an installed hunter-harness entry script, walking up from the project.
|
|
9144
|
+
|
|
9145
|
+
npx costs a full npm resolution (and a registry round-trip when the package
|
|
9146
|
+
is not a local dependency) on every call, and archive publishes it twice.
|
|
9147
|
+
Running the entry script under node directly skips all of that.
|
|
9148
|
+
"""
|
|
9149
|
+
seen: set[Path] = set()
|
|
9150
|
+
for base in (project_root, *project_root.parents):
|
|
9151
|
+
if base in seen:
|
|
9152
|
+
continue
|
|
9153
|
+
seen.add(base)
|
|
9154
|
+
candidate = base / "node_modules" / "hunter-harness" / "dist" / "bin.js"
|
|
9155
|
+
if candidate.is_file():
|
|
9156
|
+
return candidate
|
|
9157
|
+
return None
|
|
9158
|
+
|
|
9159
|
+
|
|
9160
|
+
def resolve_hunter_cli_command(project_root: Path) -> list[str]:
|
|
9161
|
+
"""Command prefix that runs the hunter-harness CLI, cheapest route first."""
|
|
9162
|
+
node = shutil.which("node")
|
|
9163
|
+
if node:
|
|
9164
|
+
entry = _installed_cli_entry(project_root)
|
|
9165
|
+
if entry is not None:
|
|
9166
|
+
return [node, str(entry)]
|
|
9167
|
+
return [*resolve_npx_launcher(), "--yes", "hunter-harness"]
|
|
9168
|
+
|
|
9169
|
+
|
|
9056
9170
|
def build_archive_package(
|
|
9057
9171
|
project_root: Path,
|
|
9058
9172
|
archive_dir: Path,
|
|
9059
9173
|
change_key: str,
|
|
9060
9174
|
*,
|
|
9061
9175
|
output_path: Path | None = None,
|
|
9176
|
+
extra_entries: dict[str, tuple[str, str, bytes]] | None = None,
|
|
9062
9177
|
) -> dict[str, Any]:
|
|
9063
|
-
"""Build the deterministic core-v1 ZIP uploaded to hunter-platform.
|
|
9178
|
+
"""Build the deterministic core-v1 ZIP uploaded to hunter-platform.
|
|
9179
|
+
|
|
9180
|
+
``extra_entries`` maps a package path to ``(role, media_type, bytes)`` for
|
|
9181
|
+
content that is *not* on disk in the archive. Republishing an older archive
|
|
9182
|
+
uses it to supply knowledge candidates that predate the candidates step,
|
|
9183
|
+
without writing into a sealed archive directory whose manifest already
|
|
9184
|
+
covers every byte in it.
|
|
9185
|
+
"""
|
|
9064
9186
|
if re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,159}", change_key) is None:
|
|
9065
9187
|
raise ValueError("change_key must be a portable path segment")
|
|
9066
9188
|
project_root, archive_dir, file_specs = _archive_core_file_specs(
|
|
@@ -9070,8 +9192,29 @@ def build_archive_package(
|
|
|
9070
9192
|
if not any(spec[1] == "reports/final/summary-data.json" for spec in file_specs):
|
|
9071
9193
|
raise ValueError("archive final summary-data.json is required")
|
|
9072
9194
|
|
|
9195
|
+
on_disk_paths = {spec[1] for spec in file_specs}
|
|
9196
|
+
injected = {
|
|
9197
|
+
package_path: value
|
|
9198
|
+
for package_path, value in (extra_entries or {}).items()
|
|
9199
|
+
if package_path not in on_disk_paths
|
|
9200
|
+
}
|
|
9201
|
+
|
|
9073
9202
|
entries: list[dict[str, Any]] = []
|
|
9074
9203
|
content_by_path: dict[str, bytes] = {}
|
|
9204
|
+
for package_path, (role, media_type, raw) in sorted(injected.items()):
|
|
9205
|
+
raw.decode("utf-8")
|
|
9206
|
+
if media_type == "application/json":
|
|
9207
|
+
raw = _archive_remote_json_bytes(raw)
|
|
9208
|
+
content_by_path[package_path] = raw
|
|
9209
|
+
entries.append(
|
|
9210
|
+
{
|
|
9211
|
+
"path": package_path,
|
|
9212
|
+
"role": role,
|
|
9213
|
+
"media_type": media_type,
|
|
9214
|
+
"content_sha256": "sha256:" + hashlib.sha256(raw).hexdigest(),
|
|
9215
|
+
"size_bytes": len(raw),
|
|
9216
|
+
}
|
|
9217
|
+
)
|
|
9075
9218
|
for source, package_path, role, media_type in sorted(file_specs, key=lambda item: item[1]):
|
|
9076
9219
|
source = _require_archive_project_path(
|
|
9077
9220
|
project_root,
|
|
@@ -9095,6 +9238,7 @@ def build_archive_package(
|
|
|
9095
9238
|
}
|
|
9096
9239
|
)
|
|
9097
9240
|
|
|
9241
|
+
entries.sort(key=lambda item: str(item["path"]))
|
|
9098
9242
|
manifest = {
|
|
9099
9243
|
"schema_version": 1,
|
|
9100
9244
|
"profile": "core-v1",
|
|
@@ -9168,6 +9312,17 @@ def build_archive_package(
|
|
|
9168
9312
|
temporary.unlink(missing_ok=True)
|
|
9169
9313
|
|
|
9170
9314
|
package_raw = output_path.read_bytes()
|
|
9315
|
+
# An upload can report knowledge_status=ready and still add nothing to the
|
|
9316
|
+
# platform when the package carries zero candidates. Counting them here is
|
|
9317
|
+
# what separates "server did not index" from "there was nothing to index".
|
|
9318
|
+
candidate_count = 0
|
|
9319
|
+
candidate_bytes = content_by_path.get("candidates/knowledge.json")
|
|
9320
|
+
if candidate_bytes is not None:
|
|
9321
|
+
try:
|
|
9322
|
+
parsed = json.loads(candidate_bytes)
|
|
9323
|
+
candidate_count = len(parsed) if isinstance(parsed, list) else 0
|
|
9324
|
+
except (json.JSONDecodeError, TypeError, ValueError):
|
|
9325
|
+
candidate_count = 0
|
|
9171
9326
|
return {
|
|
9172
9327
|
"schemaVersion": 1,
|
|
9173
9328
|
"profile": "core-v1",
|
|
@@ -9178,6 +9333,7 @@ def build_archive_package(
|
|
|
9178
9333
|
"fileCount": len(entries),
|
|
9179
9334
|
"sizeBytes": len(package_raw),
|
|
9180
9335
|
"paths": [entry["path"] for entry in entries],
|
|
9336
|
+
"knowledgeCandidateCount": candidate_count,
|
|
9181
9337
|
}
|
|
9182
9338
|
|
|
9183
9339
|
|
|
@@ -9242,7 +9398,7 @@ def auto_push_managed_snapshot(project_root: Path) -> dict[str, Any]:
|
|
|
9242
9398
|
"missingCredentials": credentials["missing"],
|
|
9243
9399
|
}
|
|
9244
9400
|
try:
|
|
9245
|
-
launcher =
|
|
9401
|
+
launcher = resolve_hunter_cli_command(project_root)
|
|
9246
9402
|
except OSError as exc:
|
|
9247
9403
|
return {
|
|
9248
9404
|
"ok": False,
|
|
@@ -9255,8 +9411,6 @@ def auto_push_managed_snapshot(project_root: Path) -> dict[str, Any]:
|
|
|
9255
9411
|
# 规则、架构地图与指令入口。
|
|
9256
9412
|
command = [
|
|
9257
9413
|
*launcher,
|
|
9258
|
-
"--yes",
|
|
9259
|
-
"hunter-harness",
|
|
9260
9414
|
"harness-push",
|
|
9261
9415
|
"--scope",
|
|
9262
9416
|
"config,rules,architecture,instructions,branch_files",
|
|
@@ -9333,6 +9487,7 @@ def auto_push_archive_core(
|
|
|
9333
9487
|
archive_dir: Path,
|
|
9334
9488
|
*,
|
|
9335
9489
|
change_key: str | None = None,
|
|
9490
|
+
extra_entries: dict[str, tuple[str, str, bytes]] | None = None,
|
|
9336
9491
|
) -> dict[str, Any]:
|
|
9337
9492
|
"""Build and upload one deterministic core ZIP after finalize.
|
|
9338
9493
|
|
|
@@ -9347,6 +9502,7 @@ def auto_push_archive_core(
|
|
|
9347
9502
|
project_root,
|
|
9348
9503
|
archive_dir,
|
|
9349
9504
|
effective_change_key,
|
|
9505
|
+
extra_entries=extra_entries,
|
|
9350
9506
|
)
|
|
9351
9507
|
except (OSError, ValueError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
9352
9508
|
return {
|
|
@@ -9396,6 +9552,7 @@ def auto_push_archive_core(
|
|
|
9396
9552
|
"packageSha256": package["packageSha256"],
|
|
9397
9553
|
"manifestSha256": package["manifestSha256"],
|
|
9398
9554
|
"fileCount": package["fileCount"],
|
|
9555
|
+
"knowledgeCandidateCount": package["knowledgeCandidateCount"],
|
|
9399
9556
|
"paths": core_paths,
|
|
9400
9557
|
"uploadStatus": "pending",
|
|
9401
9558
|
"reasonCode": initial_reason_code,
|
|
@@ -9450,6 +9607,7 @@ def auto_push_archive_core(
|
|
|
9450
9607
|
"manifestSha256": package["manifestSha256"],
|
|
9451
9608
|
"fileCount": package["fileCount"],
|
|
9452
9609
|
"sizeBytes": package["sizeBytes"],
|
|
9610
|
+
"knowledgeCandidateCount": package["knowledgeCandidateCount"],
|
|
9453
9611
|
"pending": str(pending_path),
|
|
9454
9612
|
}
|
|
9455
9613
|
|
|
@@ -9466,7 +9624,7 @@ def auto_push_archive_core(
|
|
|
9466
9624
|
# The published CLI owns credentials, project binding, durable receipt checks,
|
|
9467
9625
|
# and retries. Generic `push` is intentionally not used for archive bytes.
|
|
9468
9626
|
try:
|
|
9469
|
-
launcher =
|
|
9627
|
+
launcher = resolve_hunter_cli_command(project_root)
|
|
9470
9628
|
except OSError as exc:
|
|
9471
9629
|
reason_code = "ARCHIVE_UPLOAD_DEFERRED"
|
|
9472
9630
|
try:
|
|
@@ -9480,8 +9638,6 @@ def auto_push_archive_core(
|
|
|
9480
9638
|
}
|
|
9481
9639
|
command = [
|
|
9482
9640
|
*launcher,
|
|
9483
|
-
"--yes",
|
|
9484
|
-
"hunter-harness",
|
|
9485
9641
|
"archive",
|
|
9486
9642
|
"upload",
|
|
9487
9643
|
"--file",
|
|
@@ -10076,6 +10232,186 @@ def cmd_repair_cli(args: argparse.Namespace) -> int:
|
|
|
10076
10232
|
return code
|
|
10077
10233
|
|
|
10078
10234
|
|
|
10235
|
+
def resolve_archive_dir_for_change(
|
|
10236
|
+
project_root: Path,
|
|
10237
|
+
change_key: str,
|
|
10238
|
+
) -> tuple[Path | None, list[str]]:
|
|
10239
|
+
"""Locate the sealed archive directory for a change key.
|
|
10240
|
+
|
|
10241
|
+
Directories are named ``<YYYY-MM-DD>-<change-key>``; older layouts use the
|
|
10242
|
+
bare key. Returns the newest match plus every candidate name so an ambiguous
|
|
10243
|
+
key is reported rather than silently resolved.
|
|
10244
|
+
"""
|
|
10245
|
+
archive_root = project_root / ".harness" / "archive"
|
|
10246
|
+
if not archive_root.is_dir():
|
|
10247
|
+
return None, []
|
|
10248
|
+
matches = sorted(
|
|
10249
|
+
entry.name
|
|
10250
|
+
for entry in archive_root.iterdir()
|
|
10251
|
+
if entry.is_dir()
|
|
10252
|
+
and (entry.name == change_key or entry.name.endswith(f"-{change_key}"))
|
|
10253
|
+
)
|
|
10254
|
+
if not matches:
|
|
10255
|
+
return None, []
|
|
10256
|
+
return archive_root / matches[-1], matches
|
|
10257
|
+
|
|
10258
|
+
|
|
10259
|
+
def _republish_knowledge_entry(archive_dir: Path) -> dict[str, tuple[str, str, bytes]]:
|
|
10260
|
+
"""Knowledge candidates for archives sealed before the candidates step.
|
|
10261
|
+
|
|
10262
|
+
Generated in memory only. The archive directory is sealed: its after-manifest
|
|
10263
|
+
covers every byte in it, so writing a new file there would break byte
|
|
10264
|
+
coverage for every later reader.
|
|
10265
|
+
"""
|
|
10266
|
+
if (archive_dir / "candidates" / "knowledge.json").is_file():
|
|
10267
|
+
return {}
|
|
10268
|
+
summary_path = archive_dir / "reports" / "final" / "summary-data.json"
|
|
10269
|
+
summary = read_json(summary_path)
|
|
10270
|
+
candidates = hkc.build_knowledge_candidates(
|
|
10271
|
+
summary,
|
|
10272
|
+
change_key=str(summary.get("changeName") or archive_dir.name),
|
|
10273
|
+
archive_id=archive_dir.name,
|
|
10274
|
+
producer_version=SCHEMA_VERSION,
|
|
10275
|
+
created_at=now_iso(),
|
|
10276
|
+
)
|
|
10277
|
+
if not candidates:
|
|
10278
|
+
return {}
|
|
10279
|
+
return {
|
|
10280
|
+
"candidates/knowledge.json": (
|
|
10281
|
+
"knowledge_candidates",
|
|
10282
|
+
"application/json",
|
|
10283
|
+
hkc.render_knowledge_candidates_json(candidates).encode("utf-8"),
|
|
10284
|
+
)
|
|
10285
|
+
}
|
|
10286
|
+
|
|
10287
|
+
|
|
10288
|
+
def cmd_republish(
|
|
10289
|
+
*,
|
|
10290
|
+
change_key: str,
|
|
10291
|
+
archive_dir: Path | None,
|
|
10292
|
+
project_root: Path | None,
|
|
10293
|
+
dry_run: bool,
|
|
10294
|
+
) -> tuple[int, dict[str, Any]]:
|
|
10295
|
+
"""Rebuild and re-upload the core ZIP for an already sealed archive.
|
|
10296
|
+
|
|
10297
|
+
The normal publish path deletes the ZIP and its receipt once the server
|
|
10298
|
+
reports durable+ready, and archives produced before the outbox existed never
|
|
10299
|
+
had one, so nothing is left to retry from. Rebuilding from the sealed
|
|
10300
|
+
directory is the only route that can re-deliver an archive, and it is also
|
|
10301
|
+
how archives sealed before knowledge candidates existed get them.
|
|
10302
|
+
"""
|
|
10303
|
+
payload: dict[str, Any] = {
|
|
10304
|
+
"ok": False,
|
|
10305
|
+
"action": "republish",
|
|
10306
|
+
"changeKey": change_key,
|
|
10307
|
+
"dryRun": dry_run,
|
|
10308
|
+
}
|
|
10309
|
+
if re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,159}", change_key) is None:
|
|
10310
|
+
payload["error"] = "change key must be a portable path segment"
|
|
10311
|
+
payload["reasonCode"] = "ARCHIVE_CHANGE_KEY_INVALID"
|
|
10312
|
+
return 2, payload
|
|
10313
|
+
|
|
10314
|
+
root = (project_root or find_project_root(archive_dir or Path.cwd())).resolve()
|
|
10315
|
+
payload["projectRoot"] = str(root)
|
|
10316
|
+
matches: list[str] = []
|
|
10317
|
+
if archive_dir is None:
|
|
10318
|
+
archive_dir, matches = resolve_archive_dir_for_change(root, change_key)
|
|
10319
|
+
payload["matchedArchives"] = matches
|
|
10320
|
+
if archive_dir is None or not archive_dir.is_dir():
|
|
10321
|
+
payload["error"] = (
|
|
10322
|
+
f"no archive directory for change key {change_key} under "
|
|
10323
|
+
f"{root / '.harness' / 'archive'}"
|
|
10324
|
+
)
|
|
10325
|
+
payload["reasonCode"] = "ARCHIVE_DIR_NOT_FOUND"
|
|
10326
|
+
return 1, payload
|
|
10327
|
+
archive_dir = archive_dir.resolve()
|
|
10328
|
+
payload["archiveDir"] = str(archive_dir)
|
|
10329
|
+
if len(matches) > 1:
|
|
10330
|
+
payload["warning"] = (
|
|
10331
|
+
"多个归档目录匹配该 change key,已选用最新的一个:"
|
|
10332
|
+
+ ", ".join(matches)
|
|
10333
|
+
)
|
|
10334
|
+
|
|
10335
|
+
summary_path = archive_dir / "reports" / "final" / "summary-data.json"
|
|
10336
|
+
if not summary_path.is_file():
|
|
10337
|
+
payload["error"] = f"archive final summary-data.json missing: {summary_path}"
|
|
10338
|
+
payload["reasonCode"] = "ARCHIVE_SUMMARY_MISSING"
|
|
10339
|
+
return 1, payload
|
|
10340
|
+
|
|
10341
|
+
try:
|
|
10342
|
+
extra_entries = _republish_knowledge_entry(archive_dir)
|
|
10343
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
10344
|
+
# Knowledge is additive: never fail a re-delivery over it.
|
|
10345
|
+
payload.setdefault("warnings", []).append(
|
|
10346
|
+
f"knowledge candidate regeneration skipped: {exc}"
|
|
10347
|
+
)
|
|
10348
|
+
extra_entries = {}
|
|
10349
|
+
payload["knowledgeCandidatesInjected"] = "candidates/knowledge.json" in extra_entries
|
|
10350
|
+
|
|
10351
|
+
if dry_run:
|
|
10352
|
+
# The package builder refuses to write outside the project, so the
|
|
10353
|
+
# preview lands beside the real packages under a distinct name and is
|
|
10354
|
+
# removed again; it must never be mistaken for a pending upload.
|
|
10355
|
+
preview_path = (
|
|
10356
|
+
root / ".harness" / "state" / "local" / "archive-packages"
|
|
10357
|
+
/ f"{change_key}.preview.zip"
|
|
10358
|
+
)
|
|
10359
|
+
try:
|
|
10360
|
+
package = build_archive_package(
|
|
10361
|
+
root,
|
|
10362
|
+
archive_dir,
|
|
10363
|
+
change_key,
|
|
10364
|
+
output_path=preview_path,
|
|
10365
|
+
extra_entries=extra_entries,
|
|
10366
|
+
)
|
|
10367
|
+
except (OSError, ValueError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
10368
|
+
payload["error"] = f"archive package build failed: {exc}"
|
|
10369
|
+
payload["reasonCode"] = "ARCHIVE_PACKAGE_BUILD_FAILED"
|
|
10370
|
+
return 1, payload
|
|
10371
|
+
finally:
|
|
10372
|
+
try:
|
|
10373
|
+
preview_path.unlink(missing_ok=True)
|
|
10374
|
+
except OSError:
|
|
10375
|
+
pass
|
|
10376
|
+
payload.update(
|
|
10377
|
+
{
|
|
10378
|
+
"ok": True,
|
|
10379
|
+
"reasonCode": "ARCHIVE_REPUBLISH_PREVIEW",
|
|
10380
|
+
"fileCount": package["fileCount"],
|
|
10381
|
+
"sizeBytes": package["sizeBytes"],
|
|
10382
|
+
"knowledgeCandidateCount": package["knowledgeCandidateCount"],
|
|
10383
|
+
"packageSha256": package["packageSha256"],
|
|
10384
|
+
"manifestSha256": package["manifestSha256"],
|
|
10385
|
+
"files": package["paths"],
|
|
10386
|
+
}
|
|
10387
|
+
)
|
|
10388
|
+
return 0, payload
|
|
10389
|
+
|
|
10390
|
+
result = auto_push_archive_core(
|
|
10391
|
+
root,
|
|
10392
|
+
archive_dir,
|
|
10393
|
+
change_key=change_key,
|
|
10394
|
+
extra_entries=extra_entries,
|
|
10395
|
+
)
|
|
10396
|
+
payload["push"] = result
|
|
10397
|
+
payload["ok"] = bool(result.get("ok"))
|
|
10398
|
+
payload["reasonCode"] = result.get("reasonCode")
|
|
10399
|
+
if result.get("warning"):
|
|
10400
|
+
payload.setdefault("warnings", []).append(str(result["warning"]))
|
|
10401
|
+
return 0 if payload["ok"] else 1, payload
|
|
10402
|
+
|
|
10403
|
+
|
|
10404
|
+
def cmd_republish_cli(args: argparse.Namespace) -> int:
|
|
10405
|
+
code, payload = cmd_republish(
|
|
10406
|
+
change_key=str(args.change).strip(),
|
|
10407
|
+
archive_dir=resolve_path(args.archive_dir) if args.archive_dir else None,
|
|
10408
|
+
project_root=resolve_path(args.project) if args.project else None,
|
|
10409
|
+
dry_run=bool(args.dry_run),
|
|
10410
|
+
)
|
|
10411
|
+
emit_json(payload)
|
|
10412
|
+
return code
|
|
10413
|
+
|
|
10414
|
+
|
|
10079
10415
|
def build_parser() -> argparse.ArgumentParser:
|
|
10080
10416
|
parser = argparse.ArgumentParser(
|
|
10081
10417
|
prog="harness_archive.py",
|
|
@@ -10235,6 +10571,28 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
10235
10571
|
p_repair.add_argument("--json", action="store_true", default=True)
|
|
10236
10572
|
p_repair.set_defaults(func=cmd_repair_cli)
|
|
10237
10573
|
|
|
10574
|
+
p_republish = sub.add_parser(
|
|
10575
|
+
"republish",
|
|
10576
|
+
help=(
|
|
10577
|
+
"rebuild and re-upload the core ZIP for an already sealed archive "
|
|
10578
|
+
"(the normal path deletes the ZIP once the server confirms it)"
|
|
10579
|
+
),
|
|
10580
|
+
)
|
|
10581
|
+
p_republish.add_argument("--change", required=True, help="change key")
|
|
10582
|
+
p_republish.add_argument(
|
|
10583
|
+
"--archive-dir",
|
|
10584
|
+
default=None,
|
|
10585
|
+
help="explicit archive directory; resolved from --change when omitted",
|
|
10586
|
+
)
|
|
10587
|
+
p_republish.add_argument("--project", default=None)
|
|
10588
|
+
p_republish.add_argument(
|
|
10589
|
+
"--dry-run",
|
|
10590
|
+
action="store_true",
|
|
10591
|
+
help="build the package and report its contents without uploading",
|
|
10592
|
+
)
|
|
10593
|
+
p_republish.add_argument("--json", action="store_true", default=True)
|
|
10594
|
+
p_republish.set_defaults(func=cmd_republish_cli)
|
|
10595
|
+
|
|
10238
10596
|
return parser
|
|
10239
10597
|
|
|
10240
10598
|
|
|
@@ -217,10 +217,14 @@ def _secure_private_path(path: Path, *, directory: bool) -> str:
|
|
|
217
217
|
|
|
218
218
|
def _sensitive_candidates(root: Path) -> list[dict[str, Any]]:
|
|
219
219
|
"""Find high-confidence plaintext assignments without treating prose as a secret."""
|
|
220
|
-
|
|
220
|
+
# Compare by relative path, not by resolve() per file: this loop covers the
|
|
221
|
+
# whole change tree, and on Windows each resolve() is a realpath syscall.
|
|
222
|
+
receipt_rel = SECRET_SCAN_RECEIPT_REL.as_posix()
|
|
221
223
|
candidates: list[dict[str, Any]] = []
|
|
222
224
|
for path in sorted(root.rglob("*")):
|
|
223
|
-
if not path.is_file()
|
|
225
|
+
if not path.is_file():
|
|
226
|
+
continue
|
|
227
|
+
if path.relative_to(root).as_posix() == receipt_rel:
|
|
224
228
|
continue
|
|
225
229
|
try:
|
|
226
230
|
raw = path.read_bytes()
|
|
@@ -3,7 +3,7 @@ name: harness-apidoc
|
|
|
3
3
|
description: 从 Java 后端代码生成结构化前端 API 文档(Controller/VO/ErrorCode)。仅当用户显式调用
|
|
4
4
|
/harness-apidoc 时使用,不得自动触发。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=89d99fc9f903d46d; overlay=java; agent=cursor; do not edit -->
|
|
7
7
|
# harness-apidoc — 前端接口文档(Java overlay)
|
|
8
8
|
|
|
9
9
|
## Purpose
|