@hunter-harness/workflow-harness 0.2.22 → 0.2.24
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/README.md +2 -2
- package/harness/bundles/general/claude-code/agents/harness-evaluator.md +4 -4
- package/harness/bundles/general/claude-code/agents/harness-explorer.md +2 -2
- package/harness/bundles/general/claude-code/agents/harness-reviewer.md +2 -2
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +12 -3
- package/harness/bundles/general/claude-code/harness-archive/templates/summary-data-template.json +11 -0
- 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 +8 -4
- package/harness/bundles/general/claude-code/harness-plan/checklist.md +13 -17
- package/harness/bundles/general/claude-code/harness-plan/reference.md +24 -34
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +3 -3
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/protocols.md +3 -3
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +3 -1
- package/harness/bundles/general/claude-code/harness-submit/checklist.md +9 -3
- 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 +574 -12
- package/harness/bundles/general/claude-code/scripts/harness_deploy.py +22 -3
- package/harness/bundles/general/claude-code/scripts/harness_integration.py +126 -14
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +30 -6
- package/harness/bundles/general/claude-code/scripts/harness_preflight.py +50 -29
- package/harness/bundles/general/claude-code/scripts/harness_profile.py +33 -0
- package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +30 -8
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/README.md +2 -2
- package/harness/bundles/general/codebuddy/agents/harness-evaluator.md +5 -6
- package/harness/bundles/general/codebuddy/agents/harness-explorer.md +3 -3
- package/harness/bundles/general/codebuddy/agents/harness-reviewer.md +3 -5
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +12 -3
- package/harness/bundles/general/codebuddy/harness-archive/templates/summary-data-template.json +11 -0
- 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 +8 -4
- package/harness/bundles/general/codebuddy/harness-plan/checklist.md +13 -17
- package/harness/bundles/general/codebuddy/harness-plan/reference.md +24 -34
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +3 -3
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/protocols.md +3 -3
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +3 -1
- package/harness/bundles/general/codebuddy/harness-submit/checklist.md +9 -3
- 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 +574 -12
- package/harness/bundles/general/codebuddy/scripts/harness_deploy.py +22 -3
- package/harness/bundles/general/codebuddy/scripts/harness_integration.py +126 -14
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +30 -6
- package/harness/bundles/general/codebuddy/scripts/harness_preflight.py +50 -29
- package/harness/bundles/general/codebuddy/scripts/harness_profile.py +33 -0
- package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +30 -8
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/README.md +2 -2
- package/harness/bundles/general/codex/harness-archive/SKILL.md +12 -3
- package/harness/bundles/general/codex/harness-archive/templates/summary-data-template.json +11 -0
- 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 +8 -5
- package/harness/bundles/general/codex/harness-plan/checklist.md +13 -17
- package/harness/bundles/general/codex/harness-plan/reference.md +24 -34
- package/harness/bundles/general/codex/harness-review/SKILL.md +2 -2
- package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/protocols.md +3 -3
- package/harness/bundles/general/codex/harness-submit/SKILL.md +3 -1
- package/harness/bundles/general/codex/harness-submit/checklist.md +9 -3
- 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 +574 -12
- package/harness/bundles/general/codex/scripts/harness_deploy.py +22 -3
- package/harness/bundles/general/codex/scripts/harness_integration.py +126 -14
- package/harness/bundles/general/codex/scripts/harness_ledger.py +30 -6
- package/harness/bundles/general/codex/scripts/harness_preflight.py +50 -29
- package/harness/bundles/general/codex/scripts/harness_profile.py +33 -0
- package/harness/bundles/general/codex/scripts/harness_test_guard.py +30 -8
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/README.md +2 -2
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +12 -3
- package/harness/bundles/general/cursor/harness-archive/templates/summary-data-template.json +11 -0
- 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 +8 -5
- package/harness/bundles/general/cursor/harness-plan/checklist.md +13 -17
- package/harness/bundles/general/cursor/harness-plan/reference.md +24 -34
- package/harness/bundles/general/cursor/harness-review/SKILL.md +2 -2
- package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/protocols.md +3 -3
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +3 -1
- package/harness/bundles/general/cursor/harness-submit/checklist.md +9 -3
- 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 +574 -12
- package/harness/bundles/general/cursor/scripts/harness_deploy.py +22 -3
- package/harness/bundles/general/cursor/scripts/harness_integration.py +126 -14
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +30 -6
- package/harness/bundles/general/cursor/scripts/harness_preflight.py +50 -29
- package/harness/bundles/general/cursor/scripts/harness_profile.py +33 -0
- package/harness/bundles/general/cursor/scripts/harness_test_guard.py +30 -8
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/README.md +2 -2
- package/harness/bundles/java/claude-code/agents/harness-evaluator.md +4 -4
- package/harness/bundles/java/claude-code/agents/harness-explorer.md +2 -2
- package/harness/bundles/java/claude-code/agents/harness-reviewer.md +2 -2
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +12 -3
- package/harness/bundles/java/claude-code/harness-archive/templates/summary-data-template.json +11 -0
- 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 +8 -4
- package/harness/bundles/java/claude-code/harness-plan/checklist.md +13 -17
- package/harness/bundles/java/claude-code/harness-plan/reference.md +24 -34
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +3 -3
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/protocols.md +3 -3
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +3 -1
- package/harness/bundles/java/claude-code/harness-submit/checklist.md +9 -3
- 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 +574 -12
- package/harness/bundles/java/claude-code/scripts/harness_deploy.py +22 -3
- package/harness/bundles/java/claude-code/scripts/harness_integration.py +126 -14
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +30 -6
- package/harness/bundles/java/claude-code/scripts/harness_preflight.py +50 -29
- package/harness/bundles/java/claude-code/scripts/harness_profile.py +33 -0
- package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +30 -8
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/README.md +2 -2
- package/harness/bundles/java/codebuddy/agents/harness-evaluator.md +5 -6
- package/harness/bundles/java/codebuddy/agents/harness-explorer.md +3 -3
- package/harness/bundles/java/codebuddy/agents/harness-reviewer.md +3 -5
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +12 -3
- package/harness/bundles/java/codebuddy/harness-archive/templates/summary-data-template.json +11 -0
- 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 +8 -4
- package/harness/bundles/java/codebuddy/harness-plan/checklist.md +13 -17
- package/harness/bundles/java/codebuddy/harness-plan/reference.md +24 -34
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +3 -3
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/protocols.md +3 -3
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +3 -1
- package/harness/bundles/java/codebuddy/harness-submit/checklist.md +9 -3
- 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 +574 -12
- package/harness/bundles/java/codebuddy/scripts/harness_deploy.py +22 -3
- package/harness/bundles/java/codebuddy/scripts/harness_integration.py +126 -14
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +30 -6
- package/harness/bundles/java/codebuddy/scripts/harness_preflight.py +50 -29
- package/harness/bundles/java/codebuddy/scripts/harness_profile.py +33 -0
- package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +30 -8
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/README.md +2 -2
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +12 -3
- package/harness/bundles/java/codex/harness-archive/templates/summary-data-template.json +11 -0
- 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 +8 -5
- package/harness/bundles/java/codex/harness-plan/checklist.md +13 -17
- package/harness/bundles/java/codex/harness-plan/reference.md +24 -34
- package/harness/bundles/java/codex/harness-review/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-run/protocols.md +3 -3
- package/harness/bundles/java/codex/harness-submit/SKILL.md +3 -1
- package/harness/bundles/java/codex/harness-submit/checklist.md +9 -3
- 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 +574 -12
- package/harness/bundles/java/codex/scripts/harness_deploy.py +22 -3
- package/harness/bundles/java/codex/scripts/harness_integration.py +126 -14
- package/harness/bundles/java/codex/scripts/harness_ledger.py +30 -6
- package/harness/bundles/java/codex/scripts/harness_preflight.py +50 -29
- package/harness/bundles/java/codex/scripts/harness_profile.py +33 -0
- package/harness/bundles/java/codex/scripts/harness_test_guard.py +30 -8
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/README.md +2 -2
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +12 -3
- package/harness/bundles/java/cursor/harness-archive/templates/summary-data-template.json +11 -0
- 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 +8 -5
- package/harness/bundles/java/cursor/harness-plan/checklist.md +13 -17
- package/harness/bundles/java/cursor/harness-plan/reference.md +24 -34
- package/harness/bundles/java/cursor/harness-review/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-run/protocols.md +3 -3
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +3 -1
- package/harness/bundles/java/cursor/harness-submit/checklist.md +9 -3
- 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 +574 -12
- package/harness/bundles/java/cursor/scripts/harness_deploy.py +22 -3
- package/harness/bundles/java/cursor/scripts/harness_integration.py +126 -14
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +30 -6
- package/harness/bundles/java/cursor/scripts/harness_preflight.py +50 -29
- package/harness/bundles/java/cursor/scripts/harness_profile.py +33 -0
- package/harness/bundles/java/cursor/scripts/harness_test_guard.py +30 -8
- package/harness/manifests/general/claude-code.json +28 -28
- package/harness/manifests/general/codebuddy.json +28 -28
- package/harness/manifests/general/codex.json +25 -25
- package/harness/manifests/general/cursor.json +25 -25
- package/harness/manifests/java/claude-code.json +30 -30
- package/harness/manifests/java/codebuddy.json +30 -30
- package/harness/manifests/java/codex.json +27 -27
- package/harness/manifests/java/cursor.json +27 -27
- package/hunter-workflow-family.json +2 -2
- package/package.json +1 -1
|
@@ -331,12 +331,41 @@ def load_ci_metrics(change_dir: Path) -> tuple[dict[str, Any] | None, str | None
|
|
|
331
331
|
return None, None
|
|
332
332
|
|
|
333
333
|
|
|
334
|
-
def
|
|
335
|
-
"""Load structured product-candidate CI evidence (IA-1). Fail closed if absent."""
|
|
334
|
+
def _legacy_candidate_path(change_dir: Path) -> Path | None:
|
|
336
335
|
for relative in (
|
|
337
336
|
"evidence/product-candidate-ci.json",
|
|
338
337
|
"meta/product-candidate-ci.json",
|
|
339
338
|
"runtime/product-candidate-ci.json",
|
|
339
|
+
):
|
|
340
|
+
path = change_dir / relative
|
|
341
|
+
if path.is_file():
|
|
342
|
+
return path
|
|
343
|
+
return None
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _remote_ci_history_present(change_dir: Path) -> bool:
|
|
347
|
+
if _legacy_candidate_path(change_dir) is not None:
|
|
348
|
+
return True
|
|
349
|
+
ledger = load_ledger(change_dir) or {}
|
|
350
|
+
if any(key in ledger for key in ("productCandidateCi", "product_candidate_ci")):
|
|
351
|
+
return True
|
|
352
|
+
validations = (
|
|
353
|
+
ledger.get("validations")
|
|
354
|
+
if isinstance(ledger.get("validations"), dict)
|
|
355
|
+
else {}
|
|
356
|
+
)
|
|
357
|
+
return any(
|
|
358
|
+
key in validations
|
|
359
|
+
for key in ("productCandidateCi", "product_candidate_ci", "candidateCi")
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def load_product_candidate_ci(change_dir: Path) -> dict[str, Any] | None:
|
|
364
|
+
"""Load platform-neutral candidate evidence, then legacy CI evidence."""
|
|
365
|
+
for relative in (
|
|
366
|
+
"evidence/product-candidate-verification.json",
|
|
367
|
+
"meta/product-candidate-verification.json",
|
|
368
|
+
"runtime/product-candidate-verification.json",
|
|
340
369
|
):
|
|
341
370
|
path = change_dir / relative
|
|
342
371
|
if not path.is_file():
|
|
@@ -345,6 +374,17 @@ def load_product_candidate_ci(change_dir: Path) -> dict[str, Any] | None:
|
|
|
345
374
|
data = read_json(path)
|
|
346
375
|
except (OSError, json.JSONDecodeError):
|
|
347
376
|
continue
|
|
377
|
+
if isinstance(data, dict) and data.get("schemaVersion") == 2:
|
|
378
|
+
return data
|
|
379
|
+
|
|
380
|
+
# Legacy schema remains readable so existing projects do not need an
|
|
381
|
+
# immediate migration. It represents a remote claim, not an attestation.
|
|
382
|
+
legacy_path = _legacy_candidate_path(change_dir)
|
|
383
|
+
if legacy_path is not None:
|
|
384
|
+
try:
|
|
385
|
+
data = read_json(legacy_path)
|
|
386
|
+
except (OSError, json.JSONDecodeError):
|
|
387
|
+
data = None
|
|
348
388
|
if isinstance(data, dict) and data.get("schemaVersion") == 1:
|
|
349
389
|
return data
|
|
350
390
|
ledger = load_ledger(change_dir) or {}
|
|
@@ -361,7 +401,7 @@ def load_product_candidate_ci(change_dir: Path) -> dict[str, Any] | None:
|
|
|
361
401
|
|
|
362
402
|
|
|
363
403
|
def evaluate_product_ci_gate(change_dir: Path) -> dict[str, Any]:
|
|
364
|
-
"""
|
|
404
|
+
"""Evaluate product-candidate evidence independently of CI availability."""
|
|
365
405
|
evidence = load_product_candidate_ci(change_dir)
|
|
366
406
|
if evidence is None:
|
|
367
407
|
return {
|
|
@@ -374,6 +414,133 @@ def evaluate_product_ci_gate(change_dir: Path) -> dict[str, Any]:
|
|
|
374
414
|
),
|
|
375
415
|
"evidence": None,
|
|
376
416
|
}
|
|
417
|
+
|
|
418
|
+
if evidence.get("schemaVersion") == 2:
|
|
419
|
+
conclusion = str(evidence.get("conclusion") or "").strip().lower()
|
|
420
|
+
provider = str(evidence.get("provider") or "").strip()
|
|
421
|
+
assurance = str(evidence.get("assurance") or "").strip()
|
|
422
|
+
subject = evidence.get("subject")
|
|
423
|
+
verification = evidence.get("verification")
|
|
424
|
+
subject = subject if isinstance(subject, dict) else {}
|
|
425
|
+
verification = verification if isinstance(verification, dict) else {}
|
|
426
|
+
product_commit = str(subject.get("productCommit") or "").strip()
|
|
427
|
+
product_tree_hash = str(subject.get("productTreeHash") or "").strip()
|
|
428
|
+
command_set_hash = str(verification.get("commandSetHash") or "").strip()
|
|
429
|
+
ledger_hash = str(verification.get("ledgerHash") or "").strip()
|
|
430
|
+
common_required_fields = {
|
|
431
|
+
"provider": provider,
|
|
432
|
+
"assurance": assurance,
|
|
433
|
+
"subject.productCommit": product_commit,
|
|
434
|
+
"subject.productTreeHash": product_tree_hash,
|
|
435
|
+
}
|
|
436
|
+
allowed_assurance = {
|
|
437
|
+
"local-reproducible": 1,
|
|
438
|
+
"remote-claimed": 2,
|
|
439
|
+
"remote-attested": 3,
|
|
440
|
+
}
|
|
441
|
+
policy = load_gate_policy(change_dir) or {}
|
|
442
|
+
candidate_policy = policy.get("candidateVerification")
|
|
443
|
+
candidate_policy = (
|
|
444
|
+
candidate_policy if isinstance(candidate_policy, dict) else {}
|
|
445
|
+
)
|
|
446
|
+
minimum_assurance = str(
|
|
447
|
+
candidate_policy.get("minimumAssurance") or "local-reproducible"
|
|
448
|
+
).strip()
|
|
449
|
+
if minimum_assurance not in allowed_assurance:
|
|
450
|
+
minimum_assurance = "local-reproducible"
|
|
451
|
+
if (
|
|
452
|
+
provider == "local-harness"
|
|
453
|
+
and _remote_ci_history_present(change_dir)
|
|
454
|
+
and not bool(candidate_policy.get("allowLocalFallback"))
|
|
455
|
+
):
|
|
456
|
+
return {
|
|
457
|
+
"ok": False,
|
|
458
|
+
"code": "REMOTE_CI_DOWNGRADE_REFUSED",
|
|
459
|
+
"message": (
|
|
460
|
+
"remote CI history exists; local-harness evidence requires "
|
|
461
|
+
"candidateVerification.allowLocalFallback=true"
|
|
462
|
+
),
|
|
463
|
+
"assurance": assurance,
|
|
464
|
+
"evidence": evidence,
|
|
465
|
+
}
|
|
466
|
+
if provider == "local-harness":
|
|
467
|
+
local_required = {
|
|
468
|
+
"verification.commandSetHash": command_set_hash,
|
|
469
|
+
"verification.ledgerHash": ledger_hash,
|
|
470
|
+
"verification.toolchainHashes": verification.get("toolchainHashes"),
|
|
471
|
+
"verification.environmentHashes": verification.get("environmentHashes"),
|
|
472
|
+
"verification.dependencyHashes": verification.get("dependencyHashes"),
|
|
473
|
+
"verification.logHashes": verification.get("logHashes"),
|
|
474
|
+
}
|
|
475
|
+
required_fields = {**common_required_fields, **local_required}
|
|
476
|
+
elif provider == "remote-ci":
|
|
477
|
+
attestation = evidence.get("attestation")
|
|
478
|
+
attestation = attestation if isinstance(attestation, dict) else {}
|
|
479
|
+
if assurance == "remote-attested":
|
|
480
|
+
remote_identity_name = "verification.attestationDigest"
|
|
481
|
+
remote_identity = verification.get("attestationDigest")
|
|
482
|
+
else:
|
|
483
|
+
remote_identity_name = "verification.remoteIdentity"
|
|
484
|
+
remote_identity = (
|
|
485
|
+
verification.get("legacyEvidenceHash") or ledger_hash
|
|
486
|
+
)
|
|
487
|
+
required_fields = {
|
|
488
|
+
**common_required_fields,
|
|
489
|
+
"attestation.url": attestation.get("url"),
|
|
490
|
+
remote_identity_name: remote_identity,
|
|
491
|
+
}
|
|
492
|
+
else:
|
|
493
|
+
required_fields = common_required_fields
|
|
494
|
+
missing = [name for name, value in required_fields.items() if not value]
|
|
495
|
+
if conclusion not in {
|
|
496
|
+
"success",
|
|
497
|
+
"successful",
|
|
498
|
+
"passed",
|
|
499
|
+
"pass",
|
|
500
|
+
"green",
|
|
501
|
+
"ok",
|
|
502
|
+
}:
|
|
503
|
+
return {
|
|
504
|
+
"ok": False,
|
|
505
|
+
"code": "PRODUCT_CANDIDATE_NOT_VERIFIED",
|
|
506
|
+
"message": (
|
|
507
|
+
f"product candidate conclusion={conclusion or 'unknown'} "
|
|
508
|
+
f"provider={provider or 'unknown'}"
|
|
509
|
+
),
|
|
510
|
+
"assurance": assurance,
|
|
511
|
+
"evidence": evidence,
|
|
512
|
+
}
|
|
513
|
+
if missing or assurance not in allowed_assurance:
|
|
514
|
+
detail = ", ".join(missing) if missing else f"assurance={assurance}"
|
|
515
|
+
return {
|
|
516
|
+
"ok": False,
|
|
517
|
+
"code": "PRODUCT_CANDIDATE_NOT_VERIFIED",
|
|
518
|
+
"message": f"candidate evidence missing or invalid: {detail}",
|
|
519
|
+
"assurance": assurance,
|
|
520
|
+
"evidence": evidence,
|
|
521
|
+
}
|
|
522
|
+
if allowed_assurance[assurance] < allowed_assurance[minimum_assurance]:
|
|
523
|
+
return {
|
|
524
|
+
"ok": False,
|
|
525
|
+
"code": "PRODUCT_CANDIDATE_NOT_VERIFIED",
|
|
526
|
+
"message": (
|
|
527
|
+
f"candidate assurance={assurance} is below required "
|
|
528
|
+
f"minimumAssurance={minimum_assurance}"
|
|
529
|
+
),
|
|
530
|
+
"assurance": assurance,
|
|
531
|
+
"evidence": evidence,
|
|
532
|
+
}
|
|
533
|
+
return {
|
|
534
|
+
"ok": True,
|
|
535
|
+
"code": "PRODUCT_CANDIDATE_VERIFIED",
|
|
536
|
+
"message": (
|
|
537
|
+
f"product candidate verified provider={provider} "
|
|
538
|
+
f"assurance={assurance} commit={product_commit}"
|
|
539
|
+
),
|
|
540
|
+
"assurance": assurance,
|
|
541
|
+
"evidence": evidence,
|
|
542
|
+
}
|
|
543
|
+
|
|
377
544
|
conclusion = str(
|
|
378
545
|
evidence.get("conclusion") or evidence.get("status") or ""
|
|
379
546
|
).strip().lower()
|
|
@@ -408,6 +575,247 @@ def evaluate_product_ci_gate(change_dir: Path) -> dict[str, Any]:
|
|
|
408
575
|
}
|
|
409
576
|
|
|
410
577
|
|
|
578
|
+
def _candidate_value_hash(value: Any) -> str:
|
|
579
|
+
encoded = json.dumps(
|
|
580
|
+
value, ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
|
581
|
+
).encode("utf-8")
|
|
582
|
+
return f"sha256:{hashlib.sha256(encoded).hexdigest()}"
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _candidate_evidence_hash(change_dir: Path, value: Any) -> str:
|
|
586
|
+
if isinstance(value, str) and value.strip():
|
|
587
|
+
raw = Path(value.strip())
|
|
588
|
+
candidate = raw if raw.is_absolute() else change_dir / raw
|
|
589
|
+
try:
|
|
590
|
+
resolved = candidate.resolve()
|
|
591
|
+
resolved.relative_to(change_dir.resolve())
|
|
592
|
+
except (OSError, ValueError):
|
|
593
|
+
resolved = None
|
|
594
|
+
if resolved is not None and resolved.is_file():
|
|
595
|
+
return f"sha256:{sha256_file(resolved)}"
|
|
596
|
+
return _candidate_value_hash(value)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def migrate_legacy_candidate_evidence(
|
|
600
|
+
change_dir: Path,
|
|
601
|
+
*,
|
|
602
|
+
project: Path | None = None,
|
|
603
|
+
) -> dict[str, Any]:
|
|
604
|
+
"""Convert legacy CI JSON into an explicit remote-claimed v2 receipt."""
|
|
605
|
+
change_dir = change_dir.resolve()
|
|
606
|
+
legacy_path = _legacy_candidate_path(change_dir)
|
|
607
|
+
if legacy_path is None:
|
|
608
|
+
raise ValueError("legacy product-candidate-ci.json missing")
|
|
609
|
+
legacy = read_json(legacy_path)
|
|
610
|
+
if not isinstance(legacy, dict) or legacy.get("schemaVersion") != 1:
|
|
611
|
+
raise ValueError("legacy product-candidate-ci.json is invalid")
|
|
612
|
+
conclusion = str(
|
|
613
|
+
legacy.get("conclusion") or legacy.get("status") or ""
|
|
614
|
+
).strip().lower()
|
|
615
|
+
commit = str(legacy.get("commit") or legacy.get("headSha") or "").strip()
|
|
616
|
+
run_url = str(legacy.get("runUrl") or legacy.get("url") or "").strip()
|
|
617
|
+
if conclusion not in {
|
|
618
|
+
"success",
|
|
619
|
+
"successful",
|
|
620
|
+
"passed",
|
|
621
|
+
"pass",
|
|
622
|
+
"green",
|
|
623
|
+
"ok",
|
|
624
|
+
}:
|
|
625
|
+
raise ValueError(f"legacy CI conclusion is not successful: {conclusion}")
|
|
626
|
+
if not commit or not run_url:
|
|
627
|
+
raise ValueError("legacy CI evidence requires commit and runUrl")
|
|
628
|
+
project_root = (project or find_project_root(change_dir)).resolve()
|
|
629
|
+
tree_detail = compute_product_tree_hash_detail(project_root)
|
|
630
|
+
if tree_detail.get("truncated"):
|
|
631
|
+
raise ValueError("product tree hash truncated during legacy migration")
|
|
632
|
+
receipt = {
|
|
633
|
+
"schemaVersion": 2,
|
|
634
|
+
"provider": "remote-ci",
|
|
635
|
+
"conclusion": "success",
|
|
636
|
+
"assurance": "remote-claimed",
|
|
637
|
+
"recordedAt": now_iso(),
|
|
638
|
+
"subject": {
|
|
639
|
+
"productCommit": commit,
|
|
640
|
+
"productTreeHash": f"sha256:{tree_detail['hash']}",
|
|
641
|
+
},
|
|
642
|
+
"attestation": {
|
|
643
|
+
"url": run_url,
|
|
644
|
+
"source": "legacy-product-candidate-ci",
|
|
645
|
+
},
|
|
646
|
+
"verification": {
|
|
647
|
+
"legacyEvidenceHash": f"sha256:{sha256_file(legacy_path)}",
|
|
648
|
+
},
|
|
649
|
+
"migration": {
|
|
650
|
+
"fromSchemaVersion": 1,
|
|
651
|
+
"sourcePath": legacy_path.relative_to(change_dir).as_posix(),
|
|
652
|
+
},
|
|
653
|
+
}
|
|
654
|
+
write_json(
|
|
655
|
+
change_dir / "evidence" / "product-candidate-verification.json",
|
|
656
|
+
receipt,
|
|
657
|
+
)
|
|
658
|
+
return receipt
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def certify_local_candidate(
|
|
662
|
+
change_dir: Path,
|
|
663
|
+
*,
|
|
664
|
+
project: Path | None = None,
|
|
665
|
+
) -> dict[str, Any]:
|
|
666
|
+
"""Create a reproducible local receipt from existing ledger evidence.
|
|
667
|
+
|
|
668
|
+
This function never executes verification commands. It deliberately reuses
|
|
669
|
+
only complete, successful ledger entries so submit/archive do not rerun an
|
|
670
|
+
identical full suite merely to produce candidate evidence.
|
|
671
|
+
"""
|
|
672
|
+
change_dir = change_dir.resolve()
|
|
673
|
+
project_root = (project or find_project_root(change_dir)).resolve()
|
|
674
|
+
ledger = load_ledger(change_dir)
|
|
675
|
+
if not isinstance(ledger, dict):
|
|
676
|
+
raise ValueError("verification ledger missing")
|
|
677
|
+
validations = ledger.get("validations")
|
|
678
|
+
if not isinstance(validations, dict):
|
|
679
|
+
raise ValueError("verification ledger validations missing")
|
|
680
|
+
|
|
681
|
+
policy = load_gate_policy(change_dir) or {}
|
|
682
|
+
candidate_policy = policy.get("candidateVerification")
|
|
683
|
+
candidate_policy = (
|
|
684
|
+
candidate_policy if isinstance(candidate_policy, dict) else {}
|
|
685
|
+
)
|
|
686
|
+
if (
|
|
687
|
+
_remote_ci_history_present(change_dir)
|
|
688
|
+
and not bool(candidate_policy.get("allowLocalFallback"))
|
|
689
|
+
):
|
|
690
|
+
raise ValueError(
|
|
691
|
+
"remote CI history exists; refusing silent local fallback "
|
|
692
|
+
"(set candidateVerification.allowLocalFallback=true explicitly)"
|
|
693
|
+
)
|
|
694
|
+
required = candidate_policy.get("requiredValidations")
|
|
695
|
+
if not isinstance(required, list) or not required:
|
|
696
|
+
required = ["unitTestFull"]
|
|
697
|
+
required = [str(item).strip() for item in required if str(item).strip()]
|
|
698
|
+
|
|
699
|
+
selected: list[dict[str, Any]] = []
|
|
700
|
+
for name in required:
|
|
701
|
+
entry = validations.get(name)
|
|
702
|
+
if not isinstance(entry, dict):
|
|
703
|
+
raise ValueError(f"required validation missing: {name}")
|
|
704
|
+
missing: list[str] = []
|
|
705
|
+
if str(entry.get("status") or "").upper() != "OK":
|
|
706
|
+
missing.append("status=OK")
|
|
707
|
+
for field in (
|
|
708
|
+
"command",
|
|
709
|
+
"evidence",
|
|
710
|
+
"inputsHash",
|
|
711
|
+
"toolchainHash",
|
|
712
|
+
"environmentHash",
|
|
713
|
+
):
|
|
714
|
+
if entry.get(field) in (None, "", [], {}):
|
|
715
|
+
missing.append(field)
|
|
716
|
+
if missing:
|
|
717
|
+
raise ValueError(
|
|
718
|
+
f"required validation {name} is incomplete: {', '.join(missing)}"
|
|
719
|
+
)
|
|
720
|
+
selected.append(
|
|
721
|
+
{
|
|
722
|
+
"name": name,
|
|
723
|
+
"command": entry.get("command"),
|
|
724
|
+
"inputsHash": entry.get("inputsHash"),
|
|
725
|
+
"toolchainHash": entry.get("toolchainHash"),
|
|
726
|
+
"environmentHash": entry.get("environmentHash"),
|
|
727
|
+
"logHash": _candidate_evidence_hash(
|
|
728
|
+
change_dir, entry.get("evidence")
|
|
729
|
+
),
|
|
730
|
+
}
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
ledger_path = next(
|
|
734
|
+
(
|
|
735
|
+
change_dir / relative
|
|
736
|
+
for relative in (
|
|
737
|
+
"evidence/verification-ledger.json",
|
|
738
|
+
"verification-ledger.json",
|
|
739
|
+
)
|
|
740
|
+
if (change_dir / relative).is_file()
|
|
741
|
+
),
|
|
742
|
+
None,
|
|
743
|
+
)
|
|
744
|
+
if ledger_path is None:
|
|
745
|
+
raise ValueError("verification ledger path missing")
|
|
746
|
+
|
|
747
|
+
product_commit = str(
|
|
748
|
+
ledger.get("productCommit")
|
|
749
|
+
or ledger.get("currentHead")
|
|
750
|
+
or ledger.get("finalCommit")
|
|
751
|
+
or ""
|
|
752
|
+
).strip()
|
|
753
|
+
rc, current_head, _stderr = git_run(project_root, "rev-parse", "HEAD")
|
|
754
|
+
if rc == 0 and current_head:
|
|
755
|
+
if product_commit and not (
|
|
756
|
+
current_head.startswith(product_commit)
|
|
757
|
+
or product_commit.startswith(current_head)
|
|
758
|
+
):
|
|
759
|
+
raise ValueError(
|
|
760
|
+
"product commit is stale: "
|
|
761
|
+
f"ledger={product_commit} current={current_head}"
|
|
762
|
+
)
|
|
763
|
+
product_commit = current_head
|
|
764
|
+
if not product_commit:
|
|
765
|
+
raise ValueError("product commit unavailable")
|
|
766
|
+
|
|
767
|
+
detail = compute_product_tree_hash_detail(project_root)
|
|
768
|
+
if detail.get("truncated"):
|
|
769
|
+
raise ValueError(
|
|
770
|
+
"product tree hash truncated; narrow the project or raise the explicit limit"
|
|
771
|
+
)
|
|
772
|
+
current_tree_hash = f"sha256:{detail['hash']}"
|
|
773
|
+
recorded_tree_hash = str(ledger.get("productTreeHash") or "").strip()
|
|
774
|
+
if recorded_tree_hash and recorded_tree_hash.removeprefix(
|
|
775
|
+
"sha256:"
|
|
776
|
+
) != current_tree_hash.removeprefix("sha256:"):
|
|
777
|
+
raise ValueError(
|
|
778
|
+
"product tree is stale: "
|
|
779
|
+
f"ledger={recorded_tree_hash} current={current_tree_hash}"
|
|
780
|
+
)
|
|
781
|
+
product_tree_hash = current_tree_hash
|
|
782
|
+
|
|
783
|
+
receipt = {
|
|
784
|
+
"schemaVersion": 2,
|
|
785
|
+
"provider": "local-harness",
|
|
786
|
+
"conclusion": "success",
|
|
787
|
+
"assurance": "local-reproducible",
|
|
788
|
+
"recordedAt": now_iso(),
|
|
789
|
+
"subject": {
|
|
790
|
+
"productCommit": product_commit,
|
|
791
|
+
"productTreeHash": product_tree_hash,
|
|
792
|
+
},
|
|
793
|
+
"verification": {
|
|
794
|
+
"requiredValidations": required,
|
|
795
|
+
"reusedValidations": [item["name"] for item in selected],
|
|
796
|
+
"commandSetHash": _candidate_value_hash(
|
|
797
|
+
[item["command"] for item in selected]
|
|
798
|
+
),
|
|
799
|
+
"ledgerHash": f"sha256:{sha256_file(ledger_path)}",
|
|
800
|
+
"toolchainHashes": sorted(
|
|
801
|
+
{str(item["toolchainHash"]) for item in selected}
|
|
802
|
+
),
|
|
803
|
+
"environmentHashes": sorted(
|
|
804
|
+
{str(item["environmentHash"]) for item in selected}
|
|
805
|
+
),
|
|
806
|
+
"dependencyHashes": sorted(
|
|
807
|
+
{str(item["inputsHash"]) for item in selected}
|
|
808
|
+
),
|
|
809
|
+
"logHashes": sorted({str(item["logHash"]) for item in selected}),
|
|
810
|
+
},
|
|
811
|
+
}
|
|
812
|
+
write_json(
|
|
813
|
+
change_dir / "evidence" / "product-candidate-verification.json",
|
|
814
|
+
receipt,
|
|
815
|
+
)
|
|
816
|
+
return receipt
|
|
817
|
+
|
|
818
|
+
|
|
411
819
|
PRODUCT_TREE_HASH_FILE_LIMIT = 20_000
|
|
412
820
|
|
|
413
821
|
|
|
@@ -481,9 +889,13 @@ def resolve_product_archive_identity(
|
|
|
481
889
|
project_root = (project or find_project_root(change_dir)).resolve()
|
|
482
890
|
ledger = load_ledger(change_dir) or {}
|
|
483
891
|
ci = load_product_candidate_ci(change_dir) or {}
|
|
892
|
+
candidate_subject = (
|
|
893
|
+
ci.get("subject") if isinstance(ci.get("subject"), dict) else {}
|
|
894
|
+
)
|
|
484
895
|
product = (
|
|
485
896
|
product_commit
|
|
486
897
|
or ledger.get("productCommit")
|
|
898
|
+
or candidate_subject.get("productCommit")
|
|
487
899
|
or ci.get("commit")
|
|
488
900
|
or ledger.get("finalCommit")
|
|
489
901
|
or ""
|
|
@@ -884,8 +1296,13 @@ def check_status(
|
|
|
884
1296
|
change_dir: Path,
|
|
885
1297
|
*,
|
|
886
1298
|
allow_missing_review: bool = False,
|
|
1299
|
+
archive_intent: str = "release-candidate",
|
|
887
1300
|
) -> dict[str, Any]:
|
|
888
1301
|
"""Read-only archive preconditions. Never mutates."""
|
|
1302
|
+
if archive_intent not in {"release-candidate", "record-only"}:
|
|
1303
|
+
raise ValueError(
|
|
1304
|
+
"archive_intent must be release-candidate or record-only"
|
|
1305
|
+
)
|
|
889
1306
|
blockers: list[dict[str, str]] = []
|
|
890
1307
|
warnings: list[dict[str, str]] = []
|
|
891
1308
|
checks: dict[str, Any] = {}
|
|
@@ -894,6 +1311,7 @@ def check_status(
|
|
|
894
1311
|
return {
|
|
895
1312
|
"ok": False,
|
|
896
1313
|
"archivable": False,
|
|
1314
|
+
"releaseEligible": False,
|
|
897
1315
|
"change_dir": str(change_dir),
|
|
898
1316
|
"blockers": [{"code": "missing-change-dir", "message": f"not found: {change_dir}"}],
|
|
899
1317
|
"warnings": [],
|
|
@@ -1124,7 +1542,9 @@ def check_status(
|
|
|
1124
1542
|
else:
|
|
1125
1543
|
blockers.append(tier_issue)
|
|
1126
1544
|
|
|
1127
|
-
#
|
|
1545
|
+
# Candidate verification is a release gate, not an archive-integrity fact.
|
|
1546
|
+
# A record-only archive may persist incomplete work but can never claim
|
|
1547
|
+
# release eligibility.
|
|
1128
1548
|
ci_gate = evaluate_product_ci_gate(change_dir)
|
|
1129
1549
|
checks["product_candidate_ci"] = {
|
|
1130
1550
|
"ok": bool(ci_gate.get("ok")),
|
|
@@ -1132,12 +1552,20 @@ def check_status(
|
|
|
1132
1552
|
"evidence": ci_gate.get("evidence"),
|
|
1133
1553
|
}
|
|
1134
1554
|
if not ci_gate.get("ok"):
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
"
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1555
|
+
candidate_issue = {
|
|
1556
|
+
"code": (
|
|
1557
|
+
"PRODUCT_CANDIDATE_NOT_VERIFIED"
|
|
1558
|
+
if archive_intent == "record-only"
|
|
1559
|
+
else str(ci_gate.get("code") or "PRODUCT_CANDIDATE_NOT_VERIFIED")
|
|
1560
|
+
),
|
|
1561
|
+
"message": str(
|
|
1562
|
+
ci_gate.get("message") or "product candidate not verified"
|
|
1563
|
+
),
|
|
1564
|
+
}
|
|
1565
|
+
if archive_intent == "record-only":
|
|
1566
|
+
warnings.append(candidate_issue)
|
|
1567
|
+
else:
|
|
1568
|
+
blockers.append(candidate_issue)
|
|
1141
1569
|
|
|
1142
1570
|
# --- artifact preflight (retro §5.31) ---
|
|
1143
1571
|
# Classify artifact events before destructive finalize: informational (OK),
|
|
@@ -1168,9 +1596,20 @@ def check_status(
|
|
|
1168
1596
|
})
|
|
1169
1597
|
|
|
1170
1598
|
archivable = len(blockers) == 0
|
|
1599
|
+
candidate_codes = {
|
|
1600
|
+
"PRODUCT_CI_NOT_GREEN",
|
|
1601
|
+
"PRODUCT_CANDIDATE_NOT_VERIFIED",
|
|
1602
|
+
}
|
|
1603
|
+
archive_integrity_ok = not any(
|
|
1604
|
+
item.get("code") not in candidate_codes for item in blockers
|
|
1605
|
+
)
|
|
1171
1606
|
return {
|
|
1172
1607
|
"ok": True,
|
|
1173
1608
|
"archivable": archivable,
|
|
1609
|
+
"archiveIntent": archive_intent,
|
|
1610
|
+
"archiveIntegrity": {"ok": archive_integrity_ok},
|
|
1611
|
+
"candidateVerification": ci_gate,
|
|
1612
|
+
"releaseEligible": bool(ci_gate.get("ok")) and archivable,
|
|
1174
1613
|
"change_dir": str(change_dir),
|
|
1175
1614
|
"change_name": infer_change_name(change_dir),
|
|
1176
1615
|
"blockers": blockers,
|
|
@@ -2368,6 +2807,10 @@ def collect_summary_data(
|
|
|
2368
2807
|
"maintenanceNotes",
|
|
2369
2808
|
"knownRisks",
|
|
2370
2809
|
"manualActions",
|
|
2810
|
+
"archiveIntent",
|
|
2811
|
+
"archiveIntegrity",
|
|
2812
|
+
"candidateVerification",
|
|
2813
|
+
"releaseEligible",
|
|
2371
2814
|
):
|
|
2372
2815
|
if key in existing:
|
|
2373
2816
|
data[key] = _deepcopy_json(existing[key])
|
|
@@ -2467,6 +2910,22 @@ def collect_summary_data(
|
|
|
2467
2910
|
|
|
2468
2911
|
gate_policy = load_gate_policy(change_dir)
|
|
2469
2912
|
data["riskTier"] = str((gate_policy or {}).get("tier") or "unknown")
|
|
2913
|
+
intent_path = change_dir / "meta" / "archive-intent.json"
|
|
2914
|
+
intent_data: dict[str, Any] = {}
|
|
2915
|
+
if intent_path.is_file():
|
|
2916
|
+
try:
|
|
2917
|
+
loaded_intent = read_json(intent_path)
|
|
2918
|
+
intent_data = loaded_intent if isinstance(loaded_intent, dict) else {}
|
|
2919
|
+
sources.append("meta/archive-intent.json")
|
|
2920
|
+
except (OSError, json.JSONDecodeError):
|
|
2921
|
+
intent_data = {}
|
|
2922
|
+
candidate_gate = evaluate_product_ci_gate(change_dir)
|
|
2923
|
+
data["archiveIntent"] = str(
|
|
2924
|
+
intent_data.get("intent") or "release-candidate"
|
|
2925
|
+
)
|
|
2926
|
+
data["archiveIntegrity"] = {"ok": True}
|
|
2927
|
+
data["candidateVerification"] = candidate_gate
|
|
2928
|
+
data["releaseEligible"] = bool(candidate_gate.get("ok"))
|
|
2470
2929
|
|
|
2471
2930
|
# durations / skillCalls
|
|
2472
2931
|
if events:
|
|
@@ -3729,8 +4188,15 @@ def cmd_finalize(
|
|
|
3729
4188
|
*,
|
|
3730
4189
|
skip_ingest: bool = False,
|
|
3731
4190
|
allow_missing_review: bool = False,
|
|
4191
|
+
archive_intent: str = "release-candidate",
|
|
3732
4192
|
) -> tuple[int, dict[str, Any]]:
|
|
3733
4193
|
"""Execute the 9-step finalize pipeline. Returns (exit_code, payload)."""
|
|
4194
|
+
if archive_intent not in {"release-candidate", "record-only"}:
|
|
4195
|
+
return 1, {
|
|
4196
|
+
"ok": False,
|
|
4197
|
+
"action": "finalize",
|
|
4198
|
+
"error": "archive_intent must be release-candidate or record-only",
|
|
4199
|
+
}
|
|
3734
4200
|
warnings: list[str] = []
|
|
3735
4201
|
original_change_dir = change_dir.resolve()
|
|
3736
4202
|
change_name = original_change_dir.name
|
|
@@ -3796,6 +4262,8 @@ def cmd_finalize(
|
|
|
3796
4262
|
"operationId": operation_id,
|
|
3797
4263
|
"operationTempDir": str(operation_temp_dir),
|
|
3798
4264
|
"operationRecord": str(operation_record),
|
|
4265
|
+
"archiveIntent": archive_intent,
|
|
4266
|
+
"releaseEligible": False,
|
|
3799
4267
|
"warnings": warnings,
|
|
3800
4268
|
"steps": {},
|
|
3801
4269
|
}
|
|
@@ -3947,14 +4415,37 @@ def cmd_finalize(
|
|
|
3947
4415
|
payload["ok"] = False
|
|
3948
4416
|
return 1, payload
|
|
3949
4417
|
|
|
3950
|
-
# --- 2d. product candidate
|
|
4418
|
+
# --- 2d. product candidate verification / release eligibility ---
|
|
4419
|
+
if (
|
|
4420
|
+
not (
|
|
4421
|
+
work_dir / "evidence" / "product-candidate-verification.json"
|
|
4422
|
+
).is_file()
|
|
4423
|
+
and _legacy_candidate_path(work_dir) is not None
|
|
4424
|
+
):
|
|
4425
|
+
try:
|
|
4426
|
+
migrated = migrate_legacy_candidate_evidence(
|
|
4427
|
+
work_dir, project=project_root
|
|
4428
|
+
)
|
|
4429
|
+
payload["steps"]["candidate_evidence_migration"] = {
|
|
4430
|
+
"ok": True,
|
|
4431
|
+
"provider": migrated.get("provider"),
|
|
4432
|
+
"assurance": migrated.get("assurance"),
|
|
4433
|
+
}
|
|
4434
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
4435
|
+
payload["steps"]["candidate_evidence_migration"] = {
|
|
4436
|
+
"ok": False,
|
|
4437
|
+
"error": str(exc),
|
|
4438
|
+
}
|
|
4439
|
+
warnings.append(f"legacy candidate evidence migration failed: {exc}")
|
|
3951
4440
|
ci_gate = evaluate_product_ci_gate(work_dir)
|
|
3952
4441
|
payload["steps"]["product_candidate_ci"] = {
|
|
3953
4442
|
"ok": bool(ci_gate.get("ok")),
|
|
3954
4443
|
"code": ci_gate.get("code"),
|
|
3955
4444
|
"message": ci_gate.get("message"),
|
|
3956
4445
|
}
|
|
3957
|
-
|
|
4446
|
+
payload["candidateVerification"] = ci_gate
|
|
4447
|
+
payload["releaseEligible"] = bool(ci_gate.get("ok"))
|
|
4448
|
+
if not ci_gate.get("ok") and archive_intent == "release-candidate":
|
|
3958
4449
|
payload["error"] = str(ci_gate.get("message") or "PRODUCT_CI_NOT_GREEN")
|
|
3959
4450
|
payload["issues"] = [
|
|
3960
4451
|
{
|
|
@@ -3967,6 +4458,21 @@ def cmd_finalize(
|
|
|
3967
4458
|
payload["warnings"] = warnings
|
|
3968
4459
|
payload["ok"] = False
|
|
3969
4460
|
return 1, payload
|
|
4461
|
+
if not ci_gate.get("ok"):
|
|
4462
|
+
warnings.append(
|
|
4463
|
+
"record-only archive is not release eligible: "
|
|
4464
|
+
+ str(ci_gate.get("message") or "product candidate not verified")
|
|
4465
|
+
)
|
|
4466
|
+
write_json(
|
|
4467
|
+
work_dir / "meta" / "archive-intent.json",
|
|
4468
|
+
{
|
|
4469
|
+
"schemaVersion": 1,
|
|
4470
|
+
"intent": archive_intent,
|
|
4471
|
+
"candidateVerificationCode": ci_gate.get("code"),
|
|
4472
|
+
"releaseEligible": bool(ci_gate.get("ok")),
|
|
4473
|
+
"recordedAt": now_iso(),
|
|
4474
|
+
},
|
|
4475
|
+
)
|
|
3970
4476
|
|
|
3971
4477
|
# --- 3. candidate phase.end + freeze cutoff (RET-19 freeze-first) ---
|
|
3972
4478
|
# This terminal exists only inside the isolated operation staging tree. It
|
|
@@ -4428,12 +4934,46 @@ def cmd_status_cli(args: argparse.Namespace) -> int:
|
|
|
4428
4934
|
result = check_status(
|
|
4429
4935
|
change_dir,
|
|
4430
4936
|
allow_missing_review=bool(getattr(args, "allow_missing_review", False)),
|
|
4937
|
+
archive_intent=str(getattr(args, "intent", "release-candidate")),
|
|
4431
4938
|
)
|
|
4432
4939
|
emit_json(result)
|
|
4433
4940
|
# Checks completed → exit 0; archivable flag conveys the gate result.
|
|
4434
4941
|
return 0 if result.get("ok") else 1
|
|
4435
4942
|
|
|
4436
4943
|
|
|
4944
|
+
def cmd_certify_local_cli(args: argparse.Namespace) -> int:
|
|
4945
|
+
change_dir = resolve_path(args.change_dir)
|
|
4946
|
+
project = (
|
|
4947
|
+
resolve_path(args.project)
|
|
4948
|
+
if getattr(args, "project", None)
|
|
4949
|
+
else None
|
|
4950
|
+
)
|
|
4951
|
+
try:
|
|
4952
|
+
receipt = certify_local_candidate(change_dir, project=project)
|
|
4953
|
+
except (OSError, ValueError) as exc:
|
|
4954
|
+
emit_json(
|
|
4955
|
+
{
|
|
4956
|
+
"ok": False,
|
|
4957
|
+
"action": "certify-local",
|
|
4958
|
+
"error": str(exc),
|
|
4959
|
+
}
|
|
4960
|
+
)
|
|
4961
|
+
return 1
|
|
4962
|
+
emit_json(
|
|
4963
|
+
{
|
|
4964
|
+
"ok": True,
|
|
4965
|
+
"action": "certify-local",
|
|
4966
|
+
"evidence": receipt,
|
|
4967
|
+
"path": str(
|
|
4968
|
+
change_dir
|
|
4969
|
+
/ "evidence"
|
|
4970
|
+
/ "product-candidate-verification.json"
|
|
4971
|
+
),
|
|
4972
|
+
}
|
|
4973
|
+
)
|
|
4974
|
+
return 0
|
|
4975
|
+
|
|
4976
|
+
|
|
4437
4977
|
def cmd_finalize_cli(args: argparse.Namespace) -> int:
|
|
4438
4978
|
change_dir = resolve_path(args.change_dir)
|
|
4439
4979
|
archive_root = resolve_path(args.archive_root)
|
|
@@ -4442,6 +4982,7 @@ def cmd_finalize_cli(args: argparse.Namespace) -> int:
|
|
|
4442
4982
|
archive_root,
|
|
4443
4983
|
skip_ingest=bool(args.skip_ingest),
|
|
4444
4984
|
allow_missing_review=bool(getattr(args, "allow_missing_review", False)),
|
|
4985
|
+
archive_intent=str(getattr(args, "intent", "release-candidate")),
|
|
4445
4986
|
)
|
|
4446
4987
|
emit_json(payload)
|
|
4447
4988
|
return code
|
|
@@ -4631,8 +5172,23 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
4631
5172
|
action="store_true",
|
|
4632
5173
|
help="downgrade full-tier missing review from blocker to warning",
|
|
4633
5174
|
)
|
|
5175
|
+
p_status.add_argument(
|
|
5176
|
+
"--intent",
|
|
5177
|
+
choices=("release-candidate", "record-only"),
|
|
5178
|
+
default="release-candidate",
|
|
5179
|
+
help="release-candidate enforces candidate evidence; record-only archives facts without release eligibility",
|
|
5180
|
+
)
|
|
4634
5181
|
p_status.set_defaults(func=cmd_status_cli)
|
|
4635
5182
|
|
|
5183
|
+
p_cert = sub.add_parser(
|
|
5184
|
+
"certify-local",
|
|
5185
|
+
help="create candidate receipt from existing full ledger evidence without rerunning tests",
|
|
5186
|
+
)
|
|
5187
|
+
p_cert.add_argument("--change-dir", required=True)
|
|
5188
|
+
p_cert.add_argument("--project", default=None)
|
|
5189
|
+
p_cert.add_argument("--json", action="store_true", default=True)
|
|
5190
|
+
p_cert.set_defaults(func=cmd_certify_local_cli)
|
|
5191
|
+
|
|
4636
5192
|
p_fin = sub.add_parser("finalize", help="single-process archive finalize")
|
|
4637
5193
|
p_fin.add_argument("--change-dir", required=True)
|
|
4638
5194
|
p_fin.add_argument("--archive-root", required=True)
|
|
@@ -4642,6 +5198,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
4642
5198
|
action="store_true",
|
|
4643
5199
|
help="record override when full-tier review evidence is missing",
|
|
4644
5200
|
)
|
|
5201
|
+
p_fin.add_argument(
|
|
5202
|
+
"--intent",
|
|
5203
|
+
choices=("release-candidate", "record-only"),
|
|
5204
|
+
default="release-candidate",
|
|
5205
|
+
help="release-candidate enforces candidate evidence; record-only archives facts without release eligibility",
|
|
5206
|
+
)
|
|
4645
5207
|
p_fin.add_argument("--json", action="store_true", default=True)
|
|
4646
5208
|
p_fin.set_defaults(func=cmd_finalize_cli)
|
|
4647
5209
|
|