@hunter-harness/workflow-harness 0.2.38 → 0.2.39
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 +10 -7
- package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +24 -4
- 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-ingest/scripts/harness_knowledge.py +283 -28
- 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-review/SKILL.md +2 -2
- package/harness/bundles/general/claude-code/harness-review/reference.md +20 -5
- 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/scripts/harness_archive.py +446 -20
- package/harness/bundles/general/claude-code/scripts/harness_events.py +273 -21
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +56 -32
- package/harness/bundles/general/claude-code/scripts/harness_integration.py +197 -3
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +126 -2
- package/harness/bundles/general/claude-code/scripts/harness_report_model.py +28 -6
- package/harness/bundles/general/claude-code/scripts/harness_review.py +118 -17
- package/harness/bundles/general/claude-code/scripts/harness_service.py +219 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +10 -7
- package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +24 -4
- 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-ingest/scripts/harness_knowledge.py +283 -28
- 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-review/SKILL.md +2 -2
- package/harness/bundles/general/codebuddy/harness-review/reference.md +20 -5
- 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/scripts/harness_archive.py +446 -20
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +273 -21
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +56 -32
- package/harness/bundles/general/codebuddy/scripts/harness_integration.py +197 -3
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +126 -2
- package/harness/bundles/general/codebuddy/scripts/harness_report_model.py +28 -6
- package/harness/bundles/general/codebuddy/scripts/harness_review.py +118 -17
- package/harness/bundles/general/codebuddy/scripts/harness_service.py +219 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +10 -7
- package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +24 -4
- 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-ingest/scripts/harness_knowledge.py +283 -28
- 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-review/SKILL.md +2 -2
- package/harness/bundles/general/codex/harness-review/reference.md +20 -5
- 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/scripts/harness_archive.py +446 -20
- package/harness/bundles/general/codex/scripts/harness_events.py +273 -21
- package/harness/bundles/general/codex/scripts/harness_gate.py +56 -32
- package/harness/bundles/general/codex/scripts/harness_integration.py +197 -3
- package/harness/bundles/general/codex/scripts/harness_ledger.py +126 -2
- package/harness/bundles/general/codex/scripts/harness_report_model.py +28 -6
- package/harness/bundles/general/codex/scripts/harness_review.py +118 -17
- package/harness/bundles/general/codex/scripts/harness_service.py +219 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +10 -7
- package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +24 -4
- 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-ingest/scripts/harness_knowledge.py +283 -28
- 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-review/SKILL.md +2 -2
- package/harness/bundles/general/cursor/harness-review/reference.md +20 -5
- 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/scripts/harness_archive.py +446 -20
- package/harness/bundles/general/cursor/scripts/harness_events.py +273 -21
- package/harness/bundles/general/cursor/scripts/harness_gate.py +56 -32
- package/harness/bundles/general/cursor/scripts/harness_integration.py +197 -3
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +126 -2
- package/harness/bundles/general/cursor/scripts/harness_report_model.py +28 -6
- package/harness/bundles/general/cursor/scripts/harness_review.py +118 -17
- package/harness/bundles/general/cursor/scripts/harness_service.py +219 -0
- 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 +10 -7
- package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +24 -4
- 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-ingest/scripts/harness_knowledge.py +283 -28
- 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-review/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-review/reference.md +20 -5
- 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/scripts/harness_archive.py +446 -20
- package/harness/bundles/java/claude-code/scripts/harness_events.py +273 -21
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +56 -32
- package/harness/bundles/java/claude-code/scripts/harness_integration.py +197 -3
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +126 -2
- package/harness/bundles/java/claude-code/scripts/harness_report_model.py +28 -6
- package/harness/bundles/java/claude-code/scripts/harness_review.py +118 -17
- package/harness/bundles/java/claude-code/scripts/harness_service.py +219 -0
- 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 +10 -7
- package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +24 -4
- 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-ingest/scripts/harness_knowledge.py +283 -28
- 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-review/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-review/reference.md +20 -5
- 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/scripts/harness_archive.py +446 -20
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +273 -21
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +56 -32
- package/harness/bundles/java/codebuddy/scripts/harness_integration.py +197 -3
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +126 -2
- package/harness/bundles/java/codebuddy/scripts/harness_report_model.py +28 -6
- package/harness/bundles/java/codebuddy/scripts/harness_review.py +118 -17
- package/harness/bundles/java/codebuddy/scripts/harness_service.py +219 -0
- 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 +10 -7
- package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +24 -4
- 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-ingest/scripts/harness_knowledge.py +283 -28
- 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-review/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-review/reference.md +20 -5
- 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/scripts/harness_archive.py +446 -20
- package/harness/bundles/java/codex/scripts/harness_events.py +273 -21
- package/harness/bundles/java/codex/scripts/harness_gate.py +56 -32
- package/harness/bundles/java/codex/scripts/harness_integration.py +197 -3
- package/harness/bundles/java/codex/scripts/harness_ledger.py +126 -2
- package/harness/bundles/java/codex/scripts/harness_report_model.py +28 -6
- package/harness/bundles/java/codex/scripts/harness_review.py +118 -17
- package/harness/bundles/java/codex/scripts/harness_service.py +219 -0
- 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 +10 -7
- package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +24 -4
- 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-ingest/scripts/harness_knowledge.py +283 -28
- 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-review/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-review/reference.md +20 -5
- 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/scripts/harness_archive.py +446 -20
- package/harness/bundles/java/cursor/scripts/harness_events.py +273 -21
- package/harness/bundles/java/cursor/scripts/harness_gate.py +56 -32
- package/harness/bundles/java/cursor/scripts/harness_integration.py +197 -3
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +126 -2
- package/harness/bundles/java/cursor/scripts/harness_report_model.py +28 -6
- package/harness/bundles/java/cursor/scripts/harness_review.py +118 -17
- package/harness/bundles/java/cursor/scripts/harness_service.py +219 -0
- package/harness/manifests/general/claude-code.json +24 -24
- package/harness/manifests/general/codebuddy.json +24 -24
- package/harness/manifests/general/codex.json +24 -24
- package/harness/manifests/general/cursor.json +24 -24
- package/harness/manifests/java/claude-code.json +26 -26
- package/harness/manifests/java/codebuddy.json +26 -26
- package/harness/manifests/java/codex.json +26 -26
- package/harness/manifests/java/cursor.json +26 -26
- package/hunter-workflow-family.json +4 -4
- package/package.json +1 -1
|
@@ -36,6 +36,7 @@ if str(SCRIPTS_DIR) not in sys.path:
|
|
|
36
36
|
import harness_change # noqa: E402
|
|
37
37
|
import harness_ledger as hl # noqa: E402
|
|
38
38
|
import harness_paths # noqa: E402
|
|
39
|
+
import harness_service # noqa: E402
|
|
39
40
|
|
|
40
41
|
if hasattr(sys.stdout, "reconfigure"):
|
|
41
42
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
@@ -293,6 +294,68 @@ def _dedupe_verify_commands(
|
|
|
293
294
|
return result
|
|
294
295
|
|
|
295
296
|
|
|
297
|
+
_HEAVY_VERIFICATIONS = frozenset({"unitTestFull", "apiTest", "browserTest", "package"})
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _merge_command_kind(item: Any) -> tuple[str, str | None]:
|
|
301
|
+
"""Classify typed merge commands; legacy strings remain smoke."""
|
|
302
|
+
if not isinstance(item, dict):
|
|
303
|
+
return "smoke", None
|
|
304
|
+
verification = str(item.get("verification") or item.get("target") or "").strip() or None
|
|
305
|
+
declared = str(item.get("kind") or item.get("class") or "").strip().lower()
|
|
306
|
+
if declared in {"integrity", "smoke", "heavyweight"}:
|
|
307
|
+
return declared, verification
|
|
308
|
+
return ("heavyweight" if verification in _HEAVY_VERIFICATIONS else "smoke"), verification
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _reusable_merge_evidence(
|
|
312
|
+
ledger_path: Path | None,
|
|
313
|
+
*,
|
|
314
|
+
verification: str | None,
|
|
315
|
+
command: Sequence[str],
|
|
316
|
+
product_tree_hash: str | None,
|
|
317
|
+
reuse_context: dict[str, str],
|
|
318
|
+
) -> dict[str, Any] | None:
|
|
319
|
+
"""Return feature-tip evidence when merge content and execution key match."""
|
|
320
|
+
if ledger_path is None or verification is None or not product_tree_hash:
|
|
321
|
+
return None
|
|
322
|
+
try:
|
|
323
|
+
ledger = json.loads(ledger_path.read_text(encoding="utf-8-sig"))
|
|
324
|
+
except (OSError, json.JSONDecodeError):
|
|
325
|
+
return None
|
|
326
|
+
raw = ledger.get("verificationTargets")
|
|
327
|
+
entries = raw.values() if isinstance(raw, dict) else raw if isinstance(raw, list) else []
|
|
328
|
+
command_hash = hl.command_set_hash(" ".join(str(part) for part in command))
|
|
329
|
+
for entry in reversed(list(entries)):
|
|
330
|
+
if not isinstance(entry, dict) or entry.get("verification") != verification:
|
|
331
|
+
continue
|
|
332
|
+
if entry.get("status") != "OK":
|
|
333
|
+
continue
|
|
334
|
+
if str(entry.get("productTreeHash") or "").removeprefix("sha256:") != product_tree_hash.removeprefix("sha256:"):
|
|
335
|
+
continue
|
|
336
|
+
if str(entry.get("commandSetHash") or "") != command_hash:
|
|
337
|
+
continue
|
|
338
|
+
# Context identity is fail-closed: an evidence field may only be
|
|
339
|
+
# reused when the merge policy supplies the same current value.
|
|
340
|
+
if any(
|
|
341
|
+
str(entry.get(field) or "").strip()
|
|
342
|
+
and str(entry.get(field) or "").strip()
|
|
343
|
+
!= str(reuse_context.get(field) or "").strip()
|
|
344
|
+
for field in ("environmentHash", "toolchainHash", "lockHash", "dbSchemaHash")
|
|
345
|
+
):
|
|
346
|
+
continue
|
|
347
|
+
return {
|
|
348
|
+
"reusedFrom": {
|
|
349
|
+
"targetId": entry.get("id"),
|
|
350
|
+
"sourceCommit": entry.get("productCommit") or entry.get("currentHead"),
|
|
351
|
+
"sourceCandidate": entry.get("candidateId"),
|
|
352
|
+
},
|
|
353
|
+
"reuseReason": "product tree and canonical execution key matched",
|
|
354
|
+
"reuseKey": hl.canonical_reuse_key({**entry, "verification": verification}),
|
|
355
|
+
}
|
|
356
|
+
return None
|
|
357
|
+
|
|
358
|
+
|
|
296
359
|
def _normalize_verify_command(
|
|
297
360
|
command: Sequence[str],
|
|
298
361
|
*,
|
|
@@ -1210,6 +1273,49 @@ class IntegrationTransaction:
|
|
|
1210
1273
|
raise ValueError(f"requiredOnMerge[{index}].command missing")
|
|
1211
1274
|
return commands
|
|
1212
1275
|
|
|
1276
|
+
def _required_on_merge_plan(self) -> list[dict[str, Any]]:
|
|
1277
|
+
"""Classify profile commands without breaking the legacy command API."""
|
|
1278
|
+
import harness_profile
|
|
1279
|
+
|
|
1280
|
+
profile = harness_profile.load_profile(self.project_root)
|
|
1281
|
+
merge = profile.get("mergeVerification") if isinstance(profile, dict) else None
|
|
1282
|
+
required = merge.get("requiredOnMerge") if isinstance(merge, dict) else []
|
|
1283
|
+
if not isinstance(required, list):
|
|
1284
|
+
return []
|
|
1285
|
+
plan: list[dict[str, Any]] = []
|
|
1286
|
+
for index, item in enumerate(required):
|
|
1287
|
+
if isinstance(item, str) and item.strip():
|
|
1288
|
+
plan.append({"command": _split_verify_command(item.strip()), "kind": "smoke"})
|
|
1289
|
+
continue
|
|
1290
|
+
if not isinstance(item, dict):
|
|
1291
|
+
raise ValueError(f"requiredOnMerge[{index}] must be string or object")
|
|
1292
|
+
raw = item.get("command")
|
|
1293
|
+
if isinstance(raw, list) and raw and all(isinstance(value, str) and value for value in raw):
|
|
1294
|
+
command = [str(value) for value in raw]
|
|
1295
|
+
elif isinstance(raw, str) and raw.strip():
|
|
1296
|
+
command = _split_verify_command(raw.strip())
|
|
1297
|
+
else:
|
|
1298
|
+
raise ValueError(f"requiredOnMerge[{index}].command missing")
|
|
1299
|
+
kind, verification = _merge_command_kind(item)
|
|
1300
|
+
plan.append(
|
|
1301
|
+
{
|
|
1302
|
+
"command": command,
|
|
1303
|
+
"kind": kind,
|
|
1304
|
+
"verification": verification,
|
|
1305
|
+
"reuseContext": {
|
|
1306
|
+
field: str(item.get(field)).strip()
|
|
1307
|
+
for field in (
|
|
1308
|
+
"environmentHash",
|
|
1309
|
+
"toolchainHash",
|
|
1310
|
+
"lockHash",
|
|
1311
|
+
"dbSchemaHash",
|
|
1312
|
+
)
|
|
1313
|
+
if str(item.get(field) or "").strip()
|
|
1314
|
+
},
|
|
1315
|
+
}
|
|
1316
|
+
)
|
|
1317
|
+
return plan
|
|
1318
|
+
|
|
1213
1319
|
def verify(
|
|
1214
1320
|
self,
|
|
1215
1321
|
commands: Sequence[Sequence[str]] | None = None,
|
|
@@ -1217,12 +1323,16 @@ class IntegrationTransaction:
|
|
|
1217
1323
|
timeout_seconds: float = DEFAULT_VERIFY_TIMEOUT_SECONDS,
|
|
1218
1324
|
) -> dict[str, Any]:
|
|
1219
1325
|
commands = list(commands or [])
|
|
1220
|
-
# Wave-2 H-9: append profile mergeVerification.requiredOnMerge commands.
|
|
1221
1326
|
try:
|
|
1222
|
-
|
|
1327
|
+
profile_plan = self._required_on_merge_plan()
|
|
1223
1328
|
except Exception as exc: # noqa: BLE001 — surface as verify plan error
|
|
1224
1329
|
raise VerifyPlanMissingError(f"mergeVerification load failed: {exc}") from exc
|
|
1225
|
-
|
|
1330
|
+
plan = [
|
|
1331
|
+
{"command": list(command), "kind": "smoke", "verification": None}
|
|
1332
|
+
for command in commands
|
|
1333
|
+
]
|
|
1334
|
+
plan.extend(profile_plan)
|
|
1335
|
+
commands = _dedupe_verify_commands([item["command"] for item in plan])
|
|
1226
1336
|
if timeout_seconds <= 0:
|
|
1227
1337
|
raise VerifyPlanMissingError("verification timeout must be greater than zero")
|
|
1228
1338
|
|
|
@@ -1236,7 +1346,35 @@ class IntegrationTransaction:
|
|
|
1236
1346
|
)
|
|
1237
1347
|
intg_root = Path(journal["integrationRoot"]).resolve()
|
|
1238
1348
|
results: list[dict[str, Any]] = []
|
|
1349
|
+
merge_tree = self.runner.text(intg_root, "rev-parse", "--verify", "HEAD^{tree}")
|
|
1350
|
+
merge_tree_hash = "sha256:" + merge_tree if merge_tree else None
|
|
1351
|
+
policies = {tuple(item["command"]): item for item in plan}
|
|
1239
1352
|
for index, command in enumerate(commands):
|
|
1353
|
+
policy = policies.get(
|
|
1354
|
+
tuple(command), {"kind": "smoke", "verification": None}
|
|
1355
|
+
)
|
|
1356
|
+
reused = (
|
|
1357
|
+
_reusable_merge_evidence(
|
|
1358
|
+
self._ledger_path(),
|
|
1359
|
+
verification=policy.get("verification"),
|
|
1360
|
+
command=command,
|
|
1361
|
+
product_tree_hash=merge_tree_hash,
|
|
1362
|
+
reuse_context=policy.get("reuseContext") or {},
|
|
1363
|
+
)
|
|
1364
|
+
if policy.get("kind") == "heavyweight"
|
|
1365
|
+
else None
|
|
1366
|
+
)
|
|
1367
|
+
if reused is not None:
|
|
1368
|
+
results.append(
|
|
1369
|
+
{
|
|
1370
|
+
"command": list(command),
|
|
1371
|
+
"exitCode": 0,
|
|
1372
|
+
"reused": True,
|
|
1373
|
+
"kind": "heavyweight",
|
|
1374
|
+
**reused,
|
|
1375
|
+
}
|
|
1376
|
+
)
|
|
1377
|
+
continue
|
|
1240
1378
|
executable_command = _normalize_verify_command(command)
|
|
1241
1379
|
started = dt.datetime.now().astimezone()
|
|
1242
1380
|
try:
|
|
@@ -1579,6 +1717,51 @@ class IntegrationTransaction:
|
|
|
1579
1717
|
"residualPaths": residual_paths,
|
|
1580
1718
|
}
|
|
1581
1719
|
|
|
1720
|
+
def _persist_cleanup_snapshot(
|
|
1721
|
+
self, journal: dict[str, Any], execution_root: Path
|
|
1722
|
+
) -> dict[str, Any]:
|
|
1723
|
+
"""Persist all evidence needed to close cleanup after the root is gone."""
|
|
1724
|
+
snapshot = {
|
|
1725
|
+
"schemaVersion": 1,
|
|
1726
|
+
"createdAt": now_iso(),
|
|
1727
|
+
"changeId": self.change_id,
|
|
1728
|
+
"attemptId": self.run_id,
|
|
1729
|
+
"worktreeRoot": str(execution_root.resolve()),
|
|
1730
|
+
"executionRoot": str(execution_root.resolve()),
|
|
1731
|
+
"mergeEvidence": {
|
|
1732
|
+
key: journal.get(key)
|
|
1733
|
+
for key in (
|
|
1734
|
+
"mergeCommit",
|
|
1735
|
+
"pushedHead",
|
|
1736
|
+
"mergeFinalHash",
|
|
1737
|
+
"ciExpectedHead",
|
|
1738
|
+
"remoteHead",
|
|
1739
|
+
)
|
|
1740
|
+
},
|
|
1741
|
+
"evidenceIdentity": journal.get("evidenceIdentity"),
|
|
1742
|
+
}
|
|
1743
|
+
journal["cleanupSnapshot"] = snapshot
|
|
1744
|
+
self._save(journal)
|
|
1745
|
+
return snapshot
|
|
1746
|
+
|
|
1747
|
+
def _stop_owned_services(
|
|
1748
|
+
self, journal: dict[str, Any], execution_root: Path
|
|
1749
|
+
) -> dict[str, Any]:
|
|
1750
|
+
"""Stop only sessions explicitly owned by this transaction worktree."""
|
|
1751
|
+
result = harness_service.stop_owned_sessions(
|
|
1752
|
+
self.project_root,
|
|
1753
|
+
change_id=self.change_id,
|
|
1754
|
+
execution_root=execution_root,
|
|
1755
|
+
worktree_root=execution_root,
|
|
1756
|
+
)
|
|
1757
|
+
journal["serviceStop"] = result
|
|
1758
|
+
self._save(journal)
|
|
1759
|
+
if not result["ok"]:
|
|
1760
|
+
raise CleanupRefusedError(
|
|
1761
|
+
"owned service could not be identity-verified and stopped"
|
|
1762
|
+
)
|
|
1763
|
+
return result
|
|
1764
|
+
|
|
1582
1765
|
def cleanup(self) -> dict[str, Any]:
|
|
1583
1766
|
try:
|
|
1584
1767
|
existing = self._load()
|
|
@@ -1592,6 +1775,8 @@ class IntegrationTransaction:
|
|
|
1592
1775
|
def action(journal: dict[str, Any]) -> None:
|
|
1593
1776
|
self._assert_cleanup_consistency(journal)
|
|
1594
1777
|
intg_root = Path(journal["integrationRoot"])
|
|
1778
|
+
self._persist_cleanup_snapshot(journal, intg_root)
|
|
1779
|
+
service_stop = self._stop_owned_services(journal, intg_root)
|
|
1595
1780
|
cleanup_result: dict[str, Any] = {
|
|
1596
1781
|
"ok": True,
|
|
1597
1782
|
"code": "ALREADY_ABSENT",
|
|
@@ -1657,6 +1842,15 @@ class IntegrationTransaction:
|
|
|
1657
1842
|
"primaryHead": local_target,
|
|
1658
1843
|
"remoteHead": remote_target,
|
|
1659
1844
|
"residualPaths": cleanup_result.get("residualPaths") or [],
|
|
1845
|
+
"serviceStop": service_stop,
|
|
1846
|
+
}
|
|
1847
|
+
journal["cleanupReceipt"] = {
|
|
1848
|
+
"schemaVersion": 1,
|
|
1849
|
+
"completedAt": now_iso(),
|
|
1850
|
+
"snapshot": journal.get("cleanupSnapshot"),
|
|
1851
|
+
"worktreeRemoval": cleanup_result,
|
|
1852
|
+
"serviceStop": service_stop,
|
|
1853
|
+
"worktreeGone": not intg_root.exists(),
|
|
1660
1854
|
}
|
|
1661
1855
|
self._save(journal)
|
|
1662
1856
|
|
|
@@ -139,6 +139,55 @@ def verification_target_identity(
|
|
|
139
139
|
return "sha256:" + hashlib.sha256(payload).hexdigest()
|
|
140
140
|
|
|
141
141
|
|
|
142
|
+
def _hash_text(value: str) -> str:
|
|
143
|
+
return "sha256:" + hashlib.sha256(value.encode("utf-8")).hexdigest()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def command_set_hash(command: str) -> str:
|
|
147
|
+
"""Stable identity for the exact command set represented by one target."""
|
|
148
|
+
return _hash_text(str(command).strip())
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def product_tree_hash(project_root: Path | None) -> str | None:
|
|
152
|
+
"""Git tree identity is content provenance; a commit SHA is not."""
|
|
153
|
+
if project_root is None:
|
|
154
|
+
return None
|
|
155
|
+
tree = _git_text(project_root, "rev-parse", "--verify", "HEAD^{tree}")
|
|
156
|
+
return "sha256:" + tree if _nonempty_str(tree) else None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def lock_hash(project_root: Path | None) -> str | None:
|
|
160
|
+
"""Hash present dependency lockfiles, when the project exposes any."""
|
|
161
|
+
if project_root is None:
|
|
162
|
+
return None
|
|
163
|
+
names = (
|
|
164
|
+
"package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml",
|
|
165
|
+
"yarn.lock", "Cargo.lock", "go.sum", "poetry.lock",
|
|
166
|
+
)
|
|
167
|
+
files = [project_root / name for name in names if (project_root / name).is_file()]
|
|
168
|
+
if not files:
|
|
169
|
+
return None
|
|
170
|
+
digest = hashlib.sha256()
|
|
171
|
+
for path in files:
|
|
172
|
+
digest.update(path.name.encode("utf-8"))
|
|
173
|
+
digest.update(b"\0")
|
|
174
|
+
digest.update(hashlib.sha256(path.read_bytes()).digest())
|
|
175
|
+
return "sha256:" + digest.hexdigest()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def canonical_reuse_key(entry: dict[str, Any]) -> dict[str, str]:
|
|
179
|
+
"""Commit-independent verification identity used to authorize reuse."""
|
|
180
|
+
fields = (
|
|
181
|
+
"verification", "productTreeHash", "commandSetHash", "environmentHash",
|
|
182
|
+
"toolchainHash", "lockHash", "dbSchemaHash", "targetIdentity",
|
|
183
|
+
)
|
|
184
|
+
return {
|
|
185
|
+
field: str(entry.get(field)).strip()
|
|
186
|
+
for field in fields
|
|
187
|
+
if _nonempty_str(entry.get(field))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
|
|
142
191
|
def now_iso() -> str:
|
|
143
192
|
return dt.datetime.now().astimezone().isoformat(timespec="milliseconds")
|
|
144
193
|
|
|
@@ -1132,6 +1181,9 @@ def decide_can_reuse(
|
|
|
1132
1181
|
requested_profile_hash: str | None = None,
|
|
1133
1182
|
requested_environment_hash: str | None = None,
|
|
1134
1183
|
requested_db_schema_hash: str | None = None,
|
|
1184
|
+
requested_product_tree_hash: str | None = None,
|
|
1185
|
+
requested_command_set_hash: str | None = None,
|
|
1186
|
+
requested_lock_hash: str | None = None,
|
|
1135
1187
|
requested_target_identity: str | None = None,
|
|
1136
1188
|
required_coverage: str | None = None,
|
|
1137
1189
|
project_root: Path | str | None = None,
|
|
@@ -1162,7 +1214,40 @@ def decide_can_reuse(
|
|
|
1162
1214
|
if isinstance(target, dict) and target.get("verification") == verification
|
|
1163
1215
|
]
|
|
1164
1216
|
target_entries.sort(key=lambda item: str(item.get("finishedAt") or ""))
|
|
1165
|
-
|
|
1217
|
+
# A ledger can contain both a feature-tip and a later merge target. Select
|
|
1218
|
+
# the newest candidate matching the requested canonical key, rather than
|
|
1219
|
+
# letting an unrelated newest commit mask reusable evidence.
|
|
1220
|
+
requested_key_values = {
|
|
1221
|
+
"productTreeHash": requested_product_tree_hash,
|
|
1222
|
+
"commandSetHash": requested_command_set_hash,
|
|
1223
|
+
"toolchainHash": requested_toolchain_hash,
|
|
1224
|
+
"profileHash": requested_profile_hash,
|
|
1225
|
+
"environmentHash": requested_environment_hash,
|
|
1226
|
+
"lockHash": requested_lock_hash,
|
|
1227
|
+
"dbSchemaHash": requested_db_schema_hash,
|
|
1228
|
+
}
|
|
1229
|
+
matching_entries = [
|
|
1230
|
+
candidate
|
|
1231
|
+
for candidate in target_entries
|
|
1232
|
+
if all(
|
|
1233
|
+
not _nonempty_str(requested)
|
|
1234
|
+
or str(candidate.get(field) or "").strip().removeprefix("sha256:")
|
|
1235
|
+
== str(requested).strip().removeprefix("sha256:")
|
|
1236
|
+
for field, requested in requested_key_values.items()
|
|
1237
|
+
)
|
|
1238
|
+
and (
|
|
1239
|
+
not _nonempty_str(requested_command)
|
|
1240
|
+
or str(candidate.get("command") or "").strip()
|
|
1241
|
+
== str(requested_command).strip()
|
|
1242
|
+
)
|
|
1243
|
+
]
|
|
1244
|
+
entry = (
|
|
1245
|
+
matching_entries[-1]
|
|
1246
|
+
if matching_entries
|
|
1247
|
+
else target_entries[-1]
|
|
1248
|
+
if target_entries
|
|
1249
|
+
else None
|
|
1250
|
+
)
|
|
1166
1251
|
if not isinstance(validations, dict) and entry is None:
|
|
1167
1252
|
return {
|
|
1168
1253
|
"ok": True,
|
|
@@ -1314,12 +1399,17 @@ def decide_can_reuse(
|
|
|
1314
1399
|
"requested_command": requested_command,
|
|
1315
1400
|
}
|
|
1316
1401
|
|
|
1317
|
-
#
|
|
1402
|
+
# The reusable identity is content and execution context, never commit SHA.
|
|
1403
|
+
# productCommit remains provenance only so a feature tip can serve its no-ff
|
|
1404
|
+
# merge when the merge has exactly the same product tree.
|
|
1318
1405
|
# Only compared when both the stored entry and the request carry the field.
|
|
1319
1406
|
for field, requested, code_name in (
|
|
1407
|
+
("productTreeHash", requested_product_tree_hash, "PRODUCT_TREE_CHANGED"),
|
|
1408
|
+
("commandSetHash", requested_command_set_hash, "COMMAND_SET_CHANGED"),
|
|
1320
1409
|
("toolchainHash", requested_toolchain_hash, "TOOLCHAIN_CHANGED"),
|
|
1321
1410
|
("profileHash", requested_profile_hash, "PROFILE_CHANGED"),
|
|
1322
1411
|
("environmentHash", requested_environment_hash, "ENVIRONMENT_CHANGED"),
|
|
1412
|
+
("lockHash", requested_lock_hash, "LOCK_CHANGED"),
|
|
1323
1413
|
("dbSchemaHash", requested_db_schema_hash, "DB_SCHEMA_CHANGED"),
|
|
1324
1414
|
):
|
|
1325
1415
|
stored = entry.get(field)
|
|
@@ -1400,6 +1490,13 @@ def decide_can_reuse(
|
|
|
1400
1490
|
"inputsHash": stored_hash,
|
|
1401
1491
|
"inputsFiles": stored_files,
|
|
1402
1492
|
"evidence_summary": evidence_summary(entry),
|
|
1493
|
+
"reusedFrom": {
|
|
1494
|
+
"targetId": entry.get("id"),
|
|
1495
|
+
"sourceCommit": entry.get("productCommit") or entry.get("currentHead"),
|
|
1496
|
+
"sourceCandidate": entry.get("candidateId"),
|
|
1497
|
+
},
|
|
1498
|
+
"reuseReason": "canonical reuse key matched; commit SHA is provenance only",
|
|
1499
|
+
"reuseKey": canonical_reuse_key(entry),
|
|
1403
1500
|
"marker": "REUSED",
|
|
1404
1501
|
}
|
|
1405
1502
|
|
|
@@ -1568,6 +1665,20 @@ def cmd_can_reuse(args: argparse.Namespace) -> int:
|
|
|
1568
1665
|
requested_profile_hash=getattr(args, "profile_hash", None),
|
|
1569
1666
|
requested_environment_hash=getattr(args, "environment_hash", None),
|
|
1570
1667
|
requested_db_schema_hash=getattr(args, "db_schema_hash", None),
|
|
1668
|
+
requested_product_tree_hash=(
|
|
1669
|
+
getattr(args, "product_tree_hash", None) or product_tree_hash(project_root)
|
|
1670
|
+
),
|
|
1671
|
+
requested_command_set_hash=(
|
|
1672
|
+
getattr(args, "command_set_hash", None)
|
|
1673
|
+
or (
|
|
1674
|
+
command_set_hash(str(getattr(args, "command", "")))
|
|
1675
|
+
if _nonempty_str(getattr(args, "command", None))
|
|
1676
|
+
else None
|
|
1677
|
+
)
|
|
1678
|
+
),
|
|
1679
|
+
requested_lock_hash=(
|
|
1680
|
+
getattr(args, "lock_hash", None) or lock_hash(project_root)
|
|
1681
|
+
),
|
|
1571
1682
|
requested_target_identity=verification_target_identity(
|
|
1572
1683
|
verification, target
|
|
1573
1684
|
),
|
|
@@ -1727,6 +1838,13 @@ def cmd_record(args: argparse.Namespace) -> int:
|
|
|
1727
1838
|
val = getattr(args, attr, None)
|
|
1728
1839
|
if _nonempty_str(val):
|
|
1729
1840
|
entry[field] = str(val).strip()
|
|
1841
|
+
entry["commandSetHash"] = command_set_hash(args.command)
|
|
1842
|
+
tree_hash = product_tree_hash(project_root)
|
|
1843
|
+
if tree_hash:
|
|
1844
|
+
entry["productTreeHash"] = tree_hash
|
|
1845
|
+
dependency_lock_hash = lock_hash(project_root)
|
|
1846
|
+
if dependency_lock_hash:
|
|
1847
|
+
entry["lockHash"] = dependency_lock_hash
|
|
1730
1848
|
# package verification: record build artifact + test-reuse provenance.
|
|
1731
1849
|
if verification == "package":
|
|
1732
1850
|
if _nonempty_str(getattr(args, "deploy_artifact", None)):
|
|
@@ -1756,6 +1874,9 @@ def cmd_record(args: argparse.Namespace) -> int:
|
|
|
1756
1874
|
"toolchainHash": entry.get("toolchainHash"),
|
|
1757
1875
|
"profileHash": entry.get("profileHash"),
|
|
1758
1876
|
"environmentHash": entry.get("environmentHash"),
|
|
1877
|
+
"productTreeHash": entry.get("productTreeHash"),
|
|
1878
|
+
"commandSetHash": entry.get("commandSetHash"),
|
|
1879
|
+
"lockHash": entry.get("lockHash"),
|
|
1759
1880
|
"dbSchemaHash": entry.get("dbSchemaHash"),
|
|
1760
1881
|
}
|
|
1761
1882
|
target_id = (
|
|
@@ -1994,6 +2115,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1994
2115
|
default=None,
|
|
1995
2116
|
help="optional DB schema hash to compare against ledger entry (H-9)",
|
|
1996
2117
|
)
|
|
2118
|
+
p_reuse.add_argument("--product-tree-hash", default=None)
|
|
2119
|
+
p_reuse.add_argument("--command-set-hash", default=None)
|
|
2120
|
+
p_reuse.add_argument("--lock-hash", default=None)
|
|
1997
2121
|
p_reuse.add_argument(
|
|
1998
2122
|
"--verbose",
|
|
1999
2123
|
action="store_true",
|
|
@@ -34,21 +34,30 @@ IDENTITY_FIELDS = (
|
|
|
34
34
|
"repositoryId",
|
|
35
35
|
"changeName",
|
|
36
36
|
"baseCommit",
|
|
37
|
+
"checkpointCommit",
|
|
37
38
|
"productCommit",
|
|
39
|
+
"featureTip",
|
|
40
|
+
"mergeCommit",
|
|
41
|
+
"releaseTip",
|
|
38
42
|
"featureMergeHash",
|
|
39
43
|
"releaseTipHash",
|
|
40
44
|
"productTreeHash",
|
|
41
45
|
"environmentHash",
|
|
42
46
|
"candidateId",
|
|
43
47
|
)
|
|
44
|
-
CONTENT_HASH_FIELDS = frozenset(
|
|
48
|
+
CONTENT_HASH_FIELDS = frozenset(
|
|
49
|
+
{"productTreeHash", "commandSetHash", "environmentHash", "toolchainHash", "lockHash", "dbSchemaHash"}
|
|
50
|
+
)
|
|
45
51
|
TARGET_IDENTITY_FIELDS = (
|
|
46
52
|
"target",
|
|
47
53
|
"repositoryId",
|
|
48
54
|
"changeName",
|
|
49
|
-
"productCommit",
|
|
50
55
|
"productTreeHash",
|
|
56
|
+
"commandSetHash",
|
|
51
57
|
"environmentHash",
|
|
58
|
+
"toolchainHash",
|
|
59
|
+
"lockHash",
|
|
60
|
+
"dbSchemaHash",
|
|
52
61
|
"profile",
|
|
53
62
|
"candidateId",
|
|
54
63
|
)
|
|
@@ -92,18 +101,23 @@ def canonical_identity(summary: Mapping[str, Any]) -> dict[str, str]:
|
|
|
92
101
|
"repositoryId": ("repositoryId",),
|
|
93
102
|
"changeName": ("changeName",),
|
|
94
103
|
"baseCommit": ("baseCommit",),
|
|
104
|
+
"checkpointCommit": ("checkpointCommit",),
|
|
95
105
|
"productCommit": ("productCommit", "finalCommit"),
|
|
96
|
-
"
|
|
97
|
-
"
|
|
106
|
+
"featureTip": ("featureTip", "featureTipHash", "productCommit", "finalCommit"),
|
|
107
|
+
"mergeCommit": ("mergeCommit", "featureMergeHash"),
|
|
108
|
+
"releaseTip": ("releaseTip", "releaseTipHash", "archiveCommit", "finalCommit"),
|
|
109
|
+
"featureMergeHash": ("featureMergeHash", "mergeCommit", "featureTip", "productCommit", "finalCommit"),
|
|
110
|
+
"releaseTipHash": ("releaseTipHash", "releaseTip", "archiveCommit", "finalCommit"),
|
|
98
111
|
"productTreeHash": ("productTreeHash",),
|
|
99
112
|
"environmentHash": ("environmentHash",),
|
|
100
113
|
"candidateId": ("candidateId",),
|
|
101
114
|
}
|
|
102
115
|
identity: dict[str, str] = {}
|
|
103
116
|
for field in IDENTITY_FIELDS:
|
|
117
|
+
# Prefer changeIdentity, then nested aliases, then top-level mirrors.
|
|
104
118
|
candidates = (nested.get(field),) + tuple(
|
|
105
|
-
|
|
106
|
-
)
|
|
119
|
+
nested.get(key) for key in fallbacks[field]
|
|
120
|
+
) + tuple(summary.get(key) for key in fallbacks[field])
|
|
107
121
|
value = next(
|
|
108
122
|
(
|
|
109
123
|
str(candidate).strip()
|
|
@@ -126,7 +140,11 @@ def apply_identity_mirrors(
|
|
|
126
140
|
canonical = dict(identity or canonical_identity(summary))
|
|
127
141
|
result["changeIdentity"] = copy.deepcopy(canonical)
|
|
128
142
|
for field in (
|
|
143
|
+
"checkpointCommit",
|
|
129
144
|
"productCommit",
|
|
145
|
+
"featureTip",
|
|
146
|
+
"mergeCommit",
|
|
147
|
+
"releaseTip",
|
|
130
148
|
"featureMergeHash",
|
|
131
149
|
"releaseTipHash",
|
|
132
150
|
"productTreeHash",
|
|
@@ -138,7 +156,11 @@ def apply_identity_mirrors(
|
|
|
138
156
|
git_facts = copy.deepcopy(git_facts) if isinstance(git_facts, Mapping) else {}
|
|
139
157
|
for field in (
|
|
140
158
|
"baseCommit",
|
|
159
|
+
"checkpointCommit",
|
|
141
160
|
"productCommit",
|
|
161
|
+
"featureTip",
|
|
162
|
+
"mergeCommit",
|
|
163
|
+
"releaseTip",
|
|
142
164
|
"featureMergeHash",
|
|
143
165
|
"releaseTipHash",
|
|
144
166
|
"productTreeHash",
|