@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
|
@@ -1250,7 +1250,7 @@ def resolve_product_archive_identity(
|
|
|
1250
1250
|
product_commit: str | None = None,
|
|
1251
1251
|
archive_commit: str | None = None,
|
|
1252
1252
|
) -> dict[str, Any]:
|
|
1253
|
-
"""Resolve
|
|
1253
|
+
"""Resolve checkpoint → product/feature → merge → release identity facts."""
|
|
1254
1254
|
change_dir = change_dir.resolve()
|
|
1255
1255
|
project_root = (project or find_project_root(change_dir)).resolve()
|
|
1256
1256
|
ledger = load_ledger(change_dir) or {}
|
|
@@ -1266,14 +1266,27 @@ def resolve_product_archive_identity(
|
|
|
1266
1266
|
or ledger.get("finalCommit")
|
|
1267
1267
|
or ""
|
|
1268
1268
|
)
|
|
1269
|
-
|
|
1270
|
-
ledger.get("
|
|
1271
|
-
or ledger.get("
|
|
1269
|
+
checkpoint = str(
|
|
1270
|
+
ledger.get("checkpointCommit")
|
|
1271
|
+
or (ledger.get("checkpoint") or {}).get("commit")
|
|
1272
|
+
if isinstance(ledger.get("checkpoint"), dict)
|
|
1273
|
+
else ledger.get("checkpointCommit") or ""
|
|
1274
|
+
).strip()
|
|
1275
|
+
feature_tip = str(
|
|
1276
|
+
ledger.get("featureTip")
|
|
1277
|
+
or ledger.get("featureTipHash")
|
|
1272
1278
|
or product
|
|
1273
1279
|
).strip()
|
|
1280
|
+
merge_commit = str(
|
|
1281
|
+
ledger.get("mergeCommit")
|
|
1282
|
+
or ledger.get("featureMergeHash")
|
|
1283
|
+
or ledger.get("mergeFinalHash")
|
|
1284
|
+
or ""
|
|
1285
|
+
).strip()
|
|
1274
1286
|
code, current_head, _ = git_run(project_root, "rev-parse", "HEAD")
|
|
1275
1287
|
release_tip = str(
|
|
1276
|
-
ledger.get("
|
|
1288
|
+
ledger.get("releaseTip")
|
|
1289
|
+
or ledger.get("releaseTipHash")
|
|
1277
1290
|
or (current_head if code == 0 else "")
|
|
1278
1291
|
or ledger.get("archiveCommit")
|
|
1279
1292
|
or product
|
|
@@ -1318,8 +1331,13 @@ def resolve_product_archive_identity(
|
|
|
1318
1331
|
# Bare temp fixtures are not real project roots — avoid scanning up-tree.
|
|
1319
1332
|
tree = hashlib.sha256(b"fixture-no-product-tree").hexdigest()
|
|
1320
1333
|
identity = {
|
|
1334
|
+
"checkpointCommit": checkpoint,
|
|
1321
1335
|
"productCommit": str(product),
|
|
1322
|
-
"
|
|
1336
|
+
"featureTip": feature_tip,
|
|
1337
|
+
"mergeCommit": merge_commit,
|
|
1338
|
+
"releaseTip": release_tip,
|
|
1339
|
+
# Legacy aliases retain their prior public contract.
|
|
1340
|
+
"featureMergeHash": merge_commit or feature_tip,
|
|
1323
1341
|
"releaseTipHash": release_tip,
|
|
1324
1342
|
"productTreeHash": str(tree),
|
|
1325
1343
|
"archiveCommit": str(archive),
|
|
@@ -2312,6 +2330,90 @@ def check_status(
|
|
|
2312
2330
|
}
|
|
2313
2331
|
|
|
2314
2332
|
|
|
2333
|
+
def archive_auto_gate(
|
|
2334
|
+
change_dir: Path,
|
|
2335
|
+
*,
|
|
2336
|
+
allow_missing_review: bool = False,
|
|
2337
|
+
archive_intent: str = "release-candidate",
|
|
2338
|
+
) -> dict[str, Any]:
|
|
2339
|
+
"""Read-only proof that a post-submit/merge archive may run unattended."""
|
|
2340
|
+
status = check_status(
|
|
2341
|
+
change_dir,
|
|
2342
|
+
allow_missing_review=allow_missing_review,
|
|
2343
|
+
archive_intent=archive_intent,
|
|
2344
|
+
)
|
|
2345
|
+
if not status.get("archivable"):
|
|
2346
|
+
return {
|
|
2347
|
+
"ok": False,
|
|
2348
|
+
"action": "auto-gate",
|
|
2349
|
+
"autoArchiveAllowed": False,
|
|
2350
|
+
"reasonCode": "ARCHIVE_PRECONDITIONS_UNSATISFIED",
|
|
2351
|
+
"status": status,
|
|
2352
|
+
"nextAction": "Resolve archive status blockers, then run `harness_archive.py auto-gate` again.",
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
snapshot_path = change_dir / "meta" / "state-snapshot.json"
|
|
2356
|
+
try:
|
|
2357
|
+
snapshot = read_json(snapshot_path)
|
|
2358
|
+
except (OSError, json.JSONDecodeError, TypeError):
|
|
2359
|
+
snapshot = None
|
|
2360
|
+
if not isinstance(snapshot, dict):
|
|
2361
|
+
return {
|
|
2362
|
+
"ok": False,
|
|
2363
|
+
"action": "auto-gate",
|
|
2364
|
+
"autoArchiveAllowed": False,
|
|
2365
|
+
"reasonCode": "ARCHIVE_BOUNDARY_SNAPSHOT_MISSING",
|
|
2366
|
+
"status": status,
|
|
2367
|
+
"nextAction": "Capture the post-submit/merge archive-boundary state snapshot before auto-archiving.",
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
git_state = snapshot.get("git")
|
|
2371
|
+
snapshot_base = (
|
|
2372
|
+
(git_state.get("base") or git_state.get("baseCommit"))
|
|
2373
|
+
if isinstance(git_state, dict)
|
|
2374
|
+
else snapshot.get("base") or snapshot.get("baseCommit")
|
|
2375
|
+
)
|
|
2376
|
+
if not isinstance(snapshot_base, str) or not snapshot_base.strip():
|
|
2377
|
+
return {
|
|
2378
|
+
"ok": False,
|
|
2379
|
+
"action": "auto-gate",
|
|
2380
|
+
"autoArchiveAllowed": False,
|
|
2381
|
+
"reasonCode": "ARCHIVE_BOUNDARY_SNAPSHOT_INVALID",
|
|
2382
|
+
"status": status,
|
|
2383
|
+
"nextAction": "Capture a state snapshot containing the archive-boundary base commit after Submit or Merge.",
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
events_path = change_dir / "events.ndjson"
|
|
2387
|
+
events = he.load_events(events_path) if events_path.is_file() else []
|
|
2388
|
+
completed = any(
|
|
2389
|
+
event.get("phase") in {"submit", "merge"}
|
|
2390
|
+
and event.get("type") == "phase.end"
|
|
2391
|
+
and str(event.get("status") or "").upper() in {"OK", "WARN"}
|
|
2392
|
+
for event in events
|
|
2393
|
+
if isinstance(event, dict)
|
|
2394
|
+
)
|
|
2395
|
+
if not completed:
|
|
2396
|
+
return {
|
|
2397
|
+
"ok": False,
|
|
2398
|
+
"action": "auto-gate",
|
|
2399
|
+
"autoArchiveAllowed": False,
|
|
2400
|
+
"reasonCode": "SUBMIT_OR_MERGE_NOT_COMPLETED",
|
|
2401
|
+
"status": status,
|
|
2402
|
+
"nextAction": "Complete Submit or Merge and record its terminal phase event before auto-archiving.",
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
return {
|
|
2406
|
+
"ok": True,
|
|
2407
|
+
"action": "auto-gate",
|
|
2408
|
+
"autoArchiveAllowed": True,
|
|
2409
|
+
"reasonCode": "ARCHIVE_AUTO_GATE_SATISFIED",
|
|
2410
|
+
"status": status,
|
|
2411
|
+
"snapshotPath": str(snapshot_path),
|
|
2412
|
+
"snapshotBase": snapshot_base,
|
|
2413
|
+
"nextAction": "Run `harness_archive.py finalize` with the same change directory and intent; no AskQuestion confirmation is required.",
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
|
|
2315
2417
|
# ---------------------------------------------------------------------------
|
|
2316
2418
|
# collect
|
|
2317
2419
|
# ---------------------------------------------------------------------------
|
|
@@ -2337,6 +2439,11 @@ def _ledger_unit_tests(ledger: dict[str, Any] | None) -> dict[str, Any]:
|
|
|
2337
2439
|
"skipped": 0,
|
|
2338
2440
|
"passRate": NOT_AVAILABLE,
|
|
2339
2441
|
"source": "not-run",
|
|
2442
|
+
"latestAuthoritativeAttempt": None,
|
|
2443
|
+
"perAttemptCounts": [],
|
|
2444
|
+
"uniqueTestIdentities": [],
|
|
2445
|
+
"uniqueTestCount": 0,
|
|
2446
|
+
"rerunCount": 0,
|
|
2340
2447
|
}
|
|
2341
2448
|
if not ledger:
|
|
2342
2449
|
return empty
|
|
@@ -2460,6 +2567,87 @@ def _ledger_unit_tests(ledger: dict[str, Any] | None) -> dict[str, Any]:
|
|
|
2460
2567
|
}
|
|
2461
2568
|
if status in {"NOT_RUN", "USER_SKIPPED", "SKIPPED"}:
|
|
2462
2569
|
result["status"] = status if status != "SKIPPED" else "USER_SKIPPED"
|
|
2570
|
+
attempt_records: list[dict[str, Any]] = []
|
|
2571
|
+
for key in ("history", "attempts"):
|
|
2572
|
+
records = unit.get(key)
|
|
2573
|
+
if isinstance(records, list):
|
|
2574
|
+
attempt_records.extend(item for item in records if isinstance(item, dict))
|
|
2575
|
+
if not attempt_records:
|
|
2576
|
+
attempt_records = [unit]
|
|
2577
|
+
seen_attempts: set[str] = set()
|
|
2578
|
+
per_attempt: list[dict[str, Any]] = []
|
|
2579
|
+
unique_identities: set[str] = set()
|
|
2580
|
+
for index, attempt in enumerate(attempt_records, start=1):
|
|
2581
|
+
attempt_key = json.dumps(
|
|
2582
|
+
{
|
|
2583
|
+
"attempt": attempt.get("attempt"),
|
|
2584
|
+
"runId": attempt.get("runId") or attempt.get("run_id"),
|
|
2585
|
+
"finishedAt": attempt.get("finishedAt") or attempt.get("completedAt"),
|
|
2586
|
+
"metrics": attempt.get("metrics"),
|
|
2587
|
+
},
|
|
2588
|
+
ensure_ascii=False,
|
|
2589
|
+
sort_keys=True,
|
|
2590
|
+
default=str,
|
|
2591
|
+
)
|
|
2592
|
+
if attempt_key in seen_attempts:
|
|
2593
|
+
continue
|
|
2594
|
+
seen_attempts.add(attempt_key)
|
|
2595
|
+
attempt_metrics = (
|
|
2596
|
+
attempt.get("metrics") if isinstance(attempt.get("metrics"), dict) else attempt
|
|
2597
|
+
)
|
|
2598
|
+
attempt_total = int(
|
|
2599
|
+
attempt_metrics.get(
|
|
2600
|
+
"total",
|
|
2601
|
+
attempt_metrics.get("run", attempt_metrics.get("testsRun", 0)),
|
|
2602
|
+
)
|
|
2603
|
+
or 0
|
|
2604
|
+
)
|
|
2605
|
+
attempt_failed = int(
|
|
2606
|
+
attempt_metrics.get("failed", attempt_metrics.get("failures", 0)) or 0
|
|
2607
|
+
)
|
|
2608
|
+
attempt_errors = int(attempt_metrics.get("errors", 0) or 0)
|
|
2609
|
+
attempt_skipped = int(attempt_metrics.get("skipped", 0) or 0)
|
|
2610
|
+
attempt_passed = int(
|
|
2611
|
+
attempt_metrics.get(
|
|
2612
|
+
"passed",
|
|
2613
|
+
max(
|
|
2614
|
+
attempt_total - attempt_failed - attempt_errors - attempt_skipped,
|
|
2615
|
+
0,
|
|
2616
|
+
),
|
|
2617
|
+
)
|
|
2618
|
+
or 0
|
|
2619
|
+
)
|
|
2620
|
+
raw_identities = (
|
|
2621
|
+
attempt.get("testIdentities")
|
|
2622
|
+
or attempt_metrics.get("testIdentities")
|
|
2623
|
+
or attempt.get("testIds")
|
|
2624
|
+
or attempt_metrics.get("testIds")
|
|
2625
|
+
or []
|
|
2626
|
+
)
|
|
2627
|
+
identities = (
|
|
2628
|
+
sorted({str(item) for item in raw_identities if str(item)})
|
|
2629
|
+
if isinstance(raw_identities, list)
|
|
2630
|
+
else []
|
|
2631
|
+
)
|
|
2632
|
+
unique_identities.update(identities)
|
|
2633
|
+
per_attempt.append(
|
|
2634
|
+
{
|
|
2635
|
+
"attempt": attempt.get("attempt", index),
|
|
2636
|
+
"status": str(attempt.get("status") or status or "UNKNOWN").upper(),
|
|
2637
|
+
"run": attempt_total,
|
|
2638
|
+
"passed": attempt_passed,
|
|
2639
|
+
"failures": attempt_failed,
|
|
2640
|
+
"errors": attempt_errors,
|
|
2641
|
+
"skipped": attempt_skipped,
|
|
2642
|
+
"deselected": int(attempt_metrics.get("deselected", 0) or 0),
|
|
2643
|
+
"testIdentities": identities,
|
|
2644
|
+
}
|
|
2645
|
+
)
|
|
2646
|
+
result["perAttemptCounts"] = per_attempt
|
|
2647
|
+
result["latestAuthoritativeAttempt"] = per_attempt[-1] if per_attempt else None
|
|
2648
|
+
result["uniqueTestIdentities"] = sorted(unique_identities)
|
|
2649
|
+
result["uniqueTestCount"] = len(unique_identities)
|
|
2650
|
+
result["rerunCount"] = max(len(per_attempt) - 1, 0)
|
|
2463
2651
|
return result
|
|
2464
2652
|
|
|
2465
2653
|
|
|
@@ -2631,19 +2819,61 @@ def _risks_from_test_results(change_dir: Path) -> tuple[list[dict[str, Any]], li
|
|
|
2631
2819
|
return risks, actions
|
|
2632
2820
|
|
|
2633
2821
|
|
|
2634
|
-
def _ledger_db_compat(ledger: dict[str, Any] | None) -> str:
|
|
2822
|
+
def _ledger_db_compat(ledger: dict[str, Any] | None) -> dict[str, Any]:
|
|
2823
|
+
"""Project only typed DB compatibility evidence; command text is not proof."""
|
|
2824
|
+
empty = {"status": "NOT_RUN", "source": "not-run"}
|
|
2635
2825
|
if not ledger:
|
|
2636
|
-
return
|
|
2826
|
+
return empty
|
|
2637
2827
|
validations = ledger.get("validations") or {}
|
|
2638
2828
|
db = validations.get("dbCompatibility") or validations.get("db") or {}
|
|
2639
|
-
if isinstance(db, dict):
|
|
2640
|
-
return
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
if
|
|
2645
|
-
return
|
|
2646
|
-
|
|
2829
|
+
if not isinstance(db, dict):
|
|
2830
|
+
return empty
|
|
2831
|
+
raw_status = str(db.get("status") or "").upper()
|
|
2832
|
+
if raw_status in {"NOT_RUN", "USER_SKIPPED", "SKIPPED"}:
|
|
2833
|
+
return {"status": "NOT_RUN", "source": "typed-ledger"}
|
|
2834
|
+
if raw_status == "UNKNOWN":
|
|
2835
|
+
return {"status": "UNKNOWN", "source": "typed-ledger"}
|
|
2836
|
+
metrics = db.get("metrics")
|
|
2837
|
+
receipt = db.get("receipt")
|
|
2838
|
+
if not isinstance(metrics, dict) and isinstance(receipt, dict):
|
|
2839
|
+
candidate = receipt.get("metrics", receipt)
|
|
2840
|
+
metrics = candidate if isinstance(candidate, dict) else None
|
|
2841
|
+
if not isinstance(metrics, dict):
|
|
2842
|
+
return {"status": "EVIDENCE_MISSING", "source": "typed-ledger"}
|
|
2843
|
+
applicability = str(metrics.get("applicability") or "").upper()
|
|
2844
|
+
if applicability == "NOT_APPLICABLE":
|
|
2845
|
+
reason = str(metrics.get("reason") or "").strip()
|
|
2846
|
+
return {
|
|
2847
|
+
"status": "NOT_APPLICABLE" if reason else "EVIDENCE_MISSING",
|
|
2848
|
+
"reason": reason,
|
|
2849
|
+
"source": "typed-receipt" if isinstance(receipt, dict) else "typed-ledger",
|
|
2850
|
+
}
|
|
2851
|
+
if applicability != "APPLICABLE":
|
|
2852
|
+
return {"status": "UNKNOWN", "source": "typed-ledger"}
|
|
2853
|
+
typed_status = str(metrics.get("status") or "").upper()
|
|
2854
|
+
total = metrics.get("total")
|
|
2855
|
+
passed = metrics.get("passed")
|
|
2856
|
+
failed = metrics.get("failed")
|
|
2857
|
+
evidence_hash = metrics.get("evidenceHash")
|
|
2858
|
+
valid_counts = (
|
|
2859
|
+
all(isinstance(value, int) and not isinstance(value, bool) and value >= 0
|
|
2860
|
+
for value in (total, passed, failed))
|
|
2861
|
+
and passed + failed == total
|
|
2862
|
+
)
|
|
2863
|
+
valid_hash = isinstance(evidence_hash, str) and bool(
|
|
2864
|
+
re.fullmatch(r"sha256:[0-9a-f]{64}", evidence_hash)
|
|
2865
|
+
)
|
|
2866
|
+
if typed_status not in {"OK", "FAIL"} or not valid_counts or not valid_hash:
|
|
2867
|
+
return {"status": "EVIDENCE_MISSING", "source": "typed-ledger"}
|
|
2868
|
+
return {
|
|
2869
|
+
"status": typed_status,
|
|
2870
|
+
"applicability": applicability,
|
|
2871
|
+
"total": total,
|
|
2872
|
+
"passed": passed,
|
|
2873
|
+
"failed": failed,
|
|
2874
|
+
"evidenceHash": evidence_hash,
|
|
2875
|
+
"source": "typed-receipt" if isinstance(receipt, dict) else "typed-ledger",
|
|
2876
|
+
}
|
|
2647
2877
|
|
|
2648
2878
|
|
|
2649
2879
|
def _typed_test_metrics(entry: dict[str, Any], *, total_key: str) -> dict[str, Any]:
|
|
@@ -2691,8 +2921,10 @@ def build_verification_projection(
|
|
|
2691
2921
|
projection: dict[str, Any] = {
|
|
2692
2922
|
"unitTests": _ledger_unit_tests(effective_ledger),
|
|
2693
2923
|
"apiTests": _ledger_api_tests(effective_ledger, change_dir=change_dir),
|
|
2694
|
-
"dbCompatibility": _ledger_db_compat(effective_ledger),
|
|
2695
2924
|
}
|
|
2925
|
+
db_compatibility = _ledger_db_compat(effective_ledger)
|
|
2926
|
+
projection["dbCompatibility"] = db_compatibility["status"]
|
|
2927
|
+
projection["dbCompatibilityEvidence"] = db_compatibility
|
|
2696
2928
|
api_contract = validations.get("apiContract")
|
|
2697
2929
|
if isinstance(api_contract, dict):
|
|
2698
2930
|
projection["apiContract"] = _typed_test_metrics(
|
|
@@ -2946,6 +3178,9 @@ def _durations_from_event_phases(
|
|
|
2946
3178
|
timing = canonical.get(name)
|
|
2947
3179
|
if timing:
|
|
2948
3180
|
stage["activeExecutionMs"] = timing.get("activeExecutionMs")
|
|
3181
|
+
# HH-WF-20260730-001: recovered/superseded time (write-path
|
|
3182
|
+
# auto-seal) stays disjoint from activeExecutionMs.
|
|
3183
|
+
stage["recoveredMs"] = timing.get("recoveredMs")
|
|
2949
3184
|
stage["wallClockSpanMs"] = timing.get("wallClockSpanMs")
|
|
2950
3185
|
stage["lateEventCount"] = timing.get("lateEventCount")
|
|
2951
3186
|
stage["unclosedAttemptCount"] = timing.get("unclosedAttemptCount")
|
|
@@ -3364,18 +3599,58 @@ def _review_summary(
|
|
|
3364
3599
|
return base
|
|
3365
3600
|
|
|
3366
3601
|
|
|
3602
|
+
def _base_from_state_snapshot(change_dir: Path) -> str:
|
|
3603
|
+
"""Read archive-boundary base from meta/state-snapshot.json when present."""
|
|
3604
|
+
for relative in (
|
|
3605
|
+
Path("meta") / "state-snapshot.json",
|
|
3606
|
+
Path("state-snapshot.json"),
|
|
3607
|
+
):
|
|
3608
|
+
path = change_dir / relative
|
|
3609
|
+
if not path.is_file():
|
|
3610
|
+
continue
|
|
3611
|
+
try:
|
|
3612
|
+
payload = read_json(path)
|
|
3613
|
+
except (OSError, json.JSONDecodeError, TypeError):
|
|
3614
|
+
continue
|
|
3615
|
+
if not isinstance(payload, dict):
|
|
3616
|
+
continue
|
|
3617
|
+
git_info = payload.get("git")
|
|
3618
|
+
if isinstance(git_info, dict):
|
|
3619
|
+
base = str(git_info.get("base") or git_info.get("baseCommit") or "").strip()
|
|
3620
|
+
if base and base != NOT_AVAILABLE:
|
|
3621
|
+
return base
|
|
3622
|
+
base = str(payload.get("baseCommit") or payload.get("base") or "").strip()
|
|
3623
|
+
if base and base != NOT_AVAILABLE:
|
|
3624
|
+
return base
|
|
3625
|
+
return ""
|
|
3626
|
+
|
|
3627
|
+
|
|
3367
3628
|
def _resolve_base_commit(
|
|
3368
3629
|
ledger: dict[str, Any] | None,
|
|
3369
3630
|
change_dir: Path,
|
|
3370
3631
|
project: Path,
|
|
3371
3632
|
final: str | None,
|
|
3372
3633
|
) -> str:
|
|
3373
|
-
"""Resolve baseCommit
|
|
3634
|
+
"""Resolve baseCommit in archive-boundary order.
|
|
3635
|
+
|
|
3636
|
+
Priority (HH-ARCHIVE-20260730-001):
|
|
3637
|
+
ledger base
|
|
3638
|
+
→ archive-boundary state snapshot base
|
|
3639
|
+
→ phase context
|
|
3640
|
+
→ merge first parent
|
|
3641
|
+
→ merge-base
|
|
3642
|
+
|
|
3643
|
+
Latest merge phase context must not override the full change boundary.
|
|
3644
|
+
"""
|
|
3374
3645
|
if ledger:
|
|
3375
3646
|
base = str(ledger.get("baseCommit") or "").strip()
|
|
3376
3647
|
if base and base != NOT_AVAILABLE:
|
|
3377
3648
|
return base
|
|
3378
3649
|
|
|
3650
|
+
snapshot_base = _base_from_state_snapshot(change_dir)
|
|
3651
|
+
if snapshot_base:
|
|
3652
|
+
return snapshot_base
|
|
3653
|
+
|
|
3379
3654
|
ctx_dir = change_dir / "runtime" / "phase-context"
|
|
3380
3655
|
if ctx_dir.is_dir():
|
|
3381
3656
|
candidates = sorted(
|
|
@@ -3685,6 +3960,7 @@ def collect_summary_data(
|
|
|
3685
3960
|
"unitTests": unit,
|
|
3686
3961
|
"apiTests": api,
|
|
3687
3962
|
"dbCompatibility": db,
|
|
3963
|
+
"dbCompatibilityEvidence": projection["dbCompatibilityEvidence"],
|
|
3688
3964
|
"coverageDisplay": coverage,
|
|
3689
3965
|
}
|
|
3690
3966
|
for typed_key in ("apiContract", "browserE2E", "performance"):
|
|
@@ -3698,6 +3974,7 @@ def collect_summary_data(
|
|
|
3698
3974
|
ver.setdefault("unitTests", {})
|
|
3699
3975
|
ver.setdefault("apiTests", {})
|
|
3700
3976
|
ver.setdefault("dbCompatibility", ver.get("dbCompatibility", NOT_AVAILABLE))
|
|
3977
|
+
ver.setdefault("dbCompatibilityEvidence", {})
|
|
3701
3978
|
ver.setdefault("coverageDisplay", ver.get("coverageDisplay", NOT_AVAILABLE))
|
|
3702
3979
|
|
|
3703
3980
|
# stageStatus / finalStatus
|
|
@@ -3785,9 +4062,13 @@ def collect_summary_data(
|
|
|
3785
4062
|
# IA-1/4 identity (product vs archive)
|
|
3786
4063
|
if not for_replay:
|
|
3787
4064
|
identity = resolve_product_archive_identity(change_dir, project=project)
|
|
4065
|
+
data["checkpointCommit"] = identity.get("checkpointCommit") or ""
|
|
3788
4066
|
data["productCommit"] = identity.get("productCommit") or data.get("finalCommit")
|
|
3789
|
-
data["
|
|
3790
|
-
data["
|
|
4067
|
+
data["featureTip"] = identity.get("featureTip") or data["productCommit"]
|
|
4068
|
+
data["mergeCommit"] = identity.get("mergeCommit") or ""
|
|
4069
|
+
data["releaseTip"] = identity.get("releaseTip") or data.get("archiveCommit")
|
|
4070
|
+
data["featureMergeHash"] = data["mergeCommit"] or data.get("finalCommit")
|
|
4071
|
+
data["releaseTipHash"] = data["releaseTip"] or data.get("archiveCommit")
|
|
3791
4072
|
data["productTreeHash"] = identity.get("productTreeHash")
|
|
3792
4073
|
data["archiveCommit"] = identity.get("archiveCommit") or data.get("finalCommit")
|
|
3793
4074
|
data["environmentHash"] = identity.get("environmentHash") or NOT_AVAILABLE
|
|
@@ -3856,8 +4137,12 @@ def collect_summary_data(
|
|
|
3856
4137
|
diff_for_facts = data.get("diffStat") if isinstance(data.get("diffStat"), dict) else {}
|
|
3857
4138
|
data["gitFacts"] = {
|
|
3858
4139
|
"baseCommit": data.get("baseCommit") or "",
|
|
4140
|
+
"checkpointCommit": data.get("checkpointCommit") or "",
|
|
3859
4141
|
"finalCommit": data.get("finalCommit") or "",
|
|
3860
4142
|
"productCommit": data.get("productCommit") or "",
|
|
4143
|
+
"featureTip": data.get("featureTip") or "",
|
|
4144
|
+
"mergeCommit": data.get("mergeCommit") or "",
|
|
4145
|
+
"releaseTip": data.get("releaseTip") or "",
|
|
3861
4146
|
"featureMergeHash": data.get("featureMergeHash") or "",
|
|
3862
4147
|
"releaseTipHash": data.get("releaseTipHash") or "",
|
|
3863
4148
|
"productTreeHash": data.get("productTreeHash") or "",
|
|
@@ -5240,6 +5525,92 @@ def validate_report_adequacy(summary: dict[str, Any]) -> dict[str, Any]:
|
|
|
5240
5525
|
"message": f"final commit {final} differs from base {base} but filesChanged=0",
|
|
5241
5526
|
})
|
|
5242
5527
|
|
|
5528
|
+
# HH-ARCHIVE-20260730-001: refuse internally-consistent but truncated boundaries.
|
|
5529
|
+
identity_doc_early = (
|
|
5530
|
+
summary.get("changeIdentity")
|
|
5531
|
+
if isinstance(summary.get("changeIdentity"), dict)
|
|
5532
|
+
else {}
|
|
5533
|
+
)
|
|
5534
|
+
product_commit = str(
|
|
5535
|
+
summary.get("productCommit")
|
|
5536
|
+
or identity_doc_early.get("productCommit")
|
|
5537
|
+
or ""
|
|
5538
|
+
).strip()
|
|
5539
|
+
feature_tip = str(
|
|
5540
|
+
summary.get("featureTip")
|
|
5541
|
+
or summary.get("featureTipHash")
|
|
5542
|
+
or product_commit
|
|
5543
|
+
or ""
|
|
5544
|
+
).strip()
|
|
5545
|
+
if not feature_tip:
|
|
5546
|
+
feature_tip = str(identity_doc_early.get("productCommit") or "").strip()
|
|
5547
|
+
|
|
5548
|
+
if (
|
|
5549
|
+
base
|
|
5550
|
+
and base != NOT_AVAILABLE
|
|
5551
|
+
and feature_tip
|
|
5552
|
+
and feature_tip != NOT_AVAILABLE
|
|
5553
|
+
and base == feature_tip
|
|
5554
|
+
):
|
|
5555
|
+
issues.append({
|
|
5556
|
+
"code": "ARCHIVE_BASE_EQUALS_FEATURE_TIP",
|
|
5557
|
+
"severity": "error",
|
|
5558
|
+
"message": (
|
|
5559
|
+
f"baseCommit equals feature tip ({base}); "
|
|
5560
|
+
"archive boundary collapsed to an empty/merge-only delta"
|
|
5561
|
+
),
|
|
5562
|
+
})
|
|
5563
|
+
|
|
5564
|
+
ownership = summary.get("ownershipDiff") or summary.get("ownership")
|
|
5565
|
+
ownership = ownership if isinstance(ownership, dict) else {}
|
|
5566
|
+
ownership_files = ownership.get("files") or ownership.get("changedFiles") or []
|
|
5567
|
+
ownership_count = 0
|
|
5568
|
+
if isinstance(ownership_files, list):
|
|
5569
|
+
ownership_count = len(ownership_files)
|
|
5570
|
+
elif isinstance(ownership.get("fileCount"), int):
|
|
5571
|
+
ownership_count = int(ownership["fileCount"])
|
|
5572
|
+
changed_files = summary.get("changedFiles")
|
|
5573
|
+
changed_count = len(changed_files) if isinstance(changed_files, list) else files_changed
|
|
5574
|
+
|
|
5575
|
+
if ownership_count > 0 and changed_count == 0:
|
|
5576
|
+
issues.append({
|
|
5577
|
+
"code": "ARCHIVE_DIFF_SHRUNK_VS_OWNERSHIP",
|
|
5578
|
+
"severity": "error",
|
|
5579
|
+
"message": (
|
|
5580
|
+
f"ownership diff has {ownership_count} file(s) but report "
|
|
5581
|
+
"diff is empty; base/diff pairing is internally consistent "
|
|
5582
|
+
"but does not represent the full change"
|
|
5583
|
+
),
|
|
5584
|
+
})
|
|
5585
|
+
elif ownership_count > 0 and changed_count > 0 and changed_count * 3 < ownership_count:
|
|
5586
|
+
issues.append({
|
|
5587
|
+
"code": "ARCHIVE_DIFF_SHRUNK_VS_OWNERSHIP",
|
|
5588
|
+
"severity": "error",
|
|
5589
|
+
"message": (
|
|
5590
|
+
f"report diff covers {changed_count} file(s) but ownership "
|
|
5591
|
+
f"lists {ownership_count}; suspected merge-delta truncation"
|
|
5592
|
+
),
|
|
5593
|
+
})
|
|
5594
|
+
|
|
5595
|
+
merge_parents = summary.get("mergeParents") or summary.get("mergeParentHashes")
|
|
5596
|
+
if isinstance(merge_parents, list) and len(merge_parents) >= 2:
|
|
5597
|
+
first_parent = str(merge_parents[0] or "").strip()
|
|
5598
|
+
if (
|
|
5599
|
+
base
|
|
5600
|
+
and first_parent
|
|
5601
|
+
and base == first_parent
|
|
5602
|
+
and ownership_count > max(changed_count, files_changed)
|
|
5603
|
+
and max(changed_count, files_changed) <= 1
|
|
5604
|
+
):
|
|
5605
|
+
issues.append({
|
|
5606
|
+
"code": "ARCHIVE_NOFF_MERGE_DELTA_ONLY",
|
|
5607
|
+
"severity": "error",
|
|
5608
|
+
"message": (
|
|
5609
|
+
"no-ff merge archive appears to cover only the merge "
|
|
5610
|
+
"commit delta rather than archive-base..release-tip"
|
|
5611
|
+
),
|
|
5612
|
+
})
|
|
5613
|
+
|
|
5243
5614
|
# Typed metrics missing despite test report artifacts present.
|
|
5244
5615
|
verification = summary.get("verification") or {}
|
|
5245
5616
|
if isinstance(verification, dict):
|
|
@@ -5258,6 +5629,33 @@ def validate_report_adequacy(summary: dict[str, Any]) -> dict[str, Any]:
|
|
|
5258
5629
|
"severity": "error",
|
|
5259
5630
|
"message": "test report artifacts present but unitTests/apiTests typed metrics are empty",
|
|
5260
5631
|
})
|
|
5632
|
+
db_status = str(verification.get("dbCompatibility") or "NOT_RUN").upper()
|
|
5633
|
+
db_evidence = verification.get("dbCompatibilityEvidence")
|
|
5634
|
+
if "dbCompatibility" in verification and db_status == "EVIDENCE_MISSING":
|
|
5635
|
+
issues.append({
|
|
5636
|
+
"code": "DB_COMPATIBILITY_EVIDENCE_MISSING",
|
|
5637
|
+
"severity": "error",
|
|
5638
|
+
"message": (
|
|
5639
|
+
"DB compatibility was recorded but lacks a valid typed "
|
|
5640
|
+
"ledger/receipt evidence payload"
|
|
5641
|
+
),
|
|
5642
|
+
})
|
|
5643
|
+
elif "dbCompatibility" in verification and db_status == "NOT_RUN":
|
|
5644
|
+
issues.append({
|
|
5645
|
+
"code": "DB_COMPATIBILITY_NOT_RUN",
|
|
5646
|
+
"severity": "warning",
|
|
5647
|
+
"message": "DB compatibility validation was not run",
|
|
5648
|
+
})
|
|
5649
|
+
elif (
|
|
5650
|
+
"dbCompatibility" in verification
|
|
5651
|
+
and db_status in {"OK", "FAIL", "NOT_APPLICABLE"}
|
|
5652
|
+
and not isinstance(db_evidence, dict)
|
|
5653
|
+
):
|
|
5654
|
+
issues.append({
|
|
5655
|
+
"code": "DB_COMPATIBILITY_EVIDENCE_MISSING",
|
|
5656
|
+
"severity": "error",
|
|
5657
|
+
"message": "DB compatibility status has no typed evidence projection",
|
|
5658
|
+
})
|
|
5261
5659
|
|
|
5262
5660
|
# stageStatus contradicts event reducer.
|
|
5263
5661
|
stage = summary.get("stageStatus") or {}
|
|
@@ -7410,6 +7808,16 @@ def cmd_status_cli(args: argparse.Namespace) -> int:
|
|
|
7410
7808
|
return 0 if result.get("ok") else 1
|
|
7411
7809
|
|
|
7412
7810
|
|
|
7811
|
+
def cmd_auto_gate_cli(args: argparse.Namespace) -> int:
|
|
7812
|
+
result = archive_auto_gate(
|
|
7813
|
+
resolve_path(args.change_dir),
|
|
7814
|
+
allow_missing_review=bool(getattr(args, "allow_missing_review", False)),
|
|
7815
|
+
archive_intent=str(getattr(args, "intent", "release-candidate")),
|
|
7816
|
+
)
|
|
7817
|
+
emit_json(result)
|
|
7818
|
+
return 0 if result.get("ok") else 1
|
|
7819
|
+
|
|
7820
|
+
|
|
7413
7821
|
def cmd_certify_local_cli(args: argparse.Namespace) -> int:
|
|
7414
7822
|
change_dir = resolve_path(args.change_dir)
|
|
7415
7823
|
project = (
|
|
@@ -7649,6 +8057,24 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
7649
8057
|
)
|
|
7650
8058
|
p_status.set_defaults(func=cmd_status_cli)
|
|
7651
8059
|
|
|
8060
|
+
p_auto_gate = sub.add_parser(
|
|
8061
|
+
"auto-gate",
|
|
8062
|
+
help="read-only proof that a post-submit/merge archive may run without confirmation",
|
|
8063
|
+
)
|
|
8064
|
+
p_auto_gate.add_argument("--change-dir", required=True)
|
|
8065
|
+
p_auto_gate.add_argument("--json", action="store_true", default=True)
|
|
8066
|
+
p_auto_gate.add_argument(
|
|
8067
|
+
"--allow-missing-review",
|
|
8068
|
+
action="store_true",
|
|
8069
|
+
help="downgrade full-tier missing review from blocker to warning",
|
|
8070
|
+
)
|
|
8071
|
+
p_auto_gate.add_argument(
|
|
8072
|
+
"--intent",
|
|
8073
|
+
choices=("release-candidate", "record-only"),
|
|
8074
|
+
default="release-candidate",
|
|
8075
|
+
)
|
|
8076
|
+
p_auto_gate.set_defaults(func=cmd_auto_gate_cli)
|
|
8077
|
+
|
|
7652
8078
|
p_cert = sub.add_parser(
|
|
7653
8079
|
"certify-local",
|
|
7654
8080
|
help="create candidate receipt from existing full ledger evidence without rerunning tests",
|