@hunter-harness/workflow-harness 0.2.20 → 0.2.22
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/CONTEXT.md +20 -0
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-archive/checklist.md +14 -0
- package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +24 -3
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/checklist.md +6 -0
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-submit/checklist.md +12 -0
- 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/harness-test/checklist.md +6 -0
- package/harness/bundles/general/claude-code/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +606 -28
- package/harness/bundles/general/claude-code/scripts/harness_environment.py +459 -0
- package/harness/bundles/general/claude-code/scripts/harness_events.py +252 -15
- package/harness/bundles/general/claude-code/scripts/harness_integration.py +34 -0
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +127 -0
- package/harness/bundles/general/claude-code/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/CONTEXT.md +20 -0
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/checklist.md +14 -0
- package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +24 -3
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/checklist.md +6 -0
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-submit/checklist.md +12 -0
- 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/harness-test/checklist.md +6 -0
- package/harness/bundles/general/codebuddy/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +606 -28
- package/harness/bundles/general/codebuddy/scripts/harness_environment.py +459 -0
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +252 -15
- package/harness/bundles/general/codebuddy/scripts/harness_integration.py +34 -0
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +127 -0
- package/harness/bundles/general/codebuddy/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/CONTEXT.md +20 -0
- package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-archive/checklist.md +14 -0
- package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +24 -3
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/checklist.md +6 -0
- package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-submit/checklist.md +12 -0
- 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/harness-test/checklist.md +6 -0
- package/harness/bundles/general/codex/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/general/codex/scripts/harness_archive.py +606 -28
- package/harness/bundles/general/codex/scripts/harness_environment.py +459 -0
- package/harness/bundles/general/codex/scripts/harness_events.py +252 -15
- package/harness/bundles/general/codex/scripts/harness_integration.py +34 -0
- package/harness/bundles/general/codex/scripts/harness_ledger.py +127 -0
- package/harness/bundles/general/codex/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/CONTEXT.md +20 -0
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-archive/checklist.md +14 -0
- package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +24 -3
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/checklist.md +6 -0
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-submit/checklist.md +12 -0
- 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/harness-test/checklist.md +6 -0
- package/harness/bundles/general/cursor/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/general/cursor/scripts/harness_archive.py +606 -28
- package/harness/bundles/general/cursor/scripts/harness_environment.py +459 -0
- package/harness/bundles/general/cursor/scripts/harness_events.py +252 -15
- package/harness/bundles/general/cursor/scripts/harness_integration.py +34 -0
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +127 -0
- package/harness/bundles/general/cursor/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/CONTEXT.md +20 -0
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/checklist.md +14 -0
- package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +24 -3
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/checklist.md +12 -0
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +606 -28
- package/harness/bundles/java/claude-code/scripts/harness_environment.py +459 -0
- package/harness/bundles/java/claude-code/scripts/harness_events.py +252 -15
- package/harness/bundles/java/claude-code/scripts/harness_integration.py +34 -0
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +127 -0
- package/harness/bundles/java/claude-code/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/CONTEXT.md +20 -0
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/checklist.md +14 -0
- package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +24 -3
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/checklist.md +12 -0
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +606 -28
- package/harness/bundles/java/codebuddy/scripts/harness_environment.py +459 -0
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +252 -15
- package/harness/bundles/java/codebuddy/scripts/harness_integration.py +34 -0
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +127 -0
- package/harness/bundles/java/codebuddy/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/CONTEXT.md +20 -0
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/checklist.md +14 -0
- package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +24 -3
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-submit/checklist.md +12 -0
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codex/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/java/codex/scripts/harness_archive.py +606 -28
- package/harness/bundles/java/codex/scripts/harness_environment.py +459 -0
- package/harness/bundles/java/codex/scripts/harness_events.py +252 -15
- package/harness/bundles/java/codex/scripts/harness_integration.py +34 -0
- package/harness/bundles/java/codex/scripts/harness_ledger.py +127 -0
- package/harness/bundles/java/codex/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/CONTEXT.md +20 -0
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/checklist.md +14 -0
- package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +24 -3
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-submit/checklist.md +12 -0
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/cursor/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/java/cursor/scripts/harness_archive.py +606 -28
- package/harness/bundles/java/cursor/scripts/harness_environment.py +459 -0
- package/harness/bundles/java/cursor/scripts/harness_events.py +252 -15
- package/harness/bundles/java/cursor/scripts/harness_integration.py +34 -0
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +127 -0
- package/harness/bundles/java/cursor/scripts/harness_migration_head.py +220 -0
- package/harness/manifests/general/claude-code.json +33 -21
- package/harness/manifests/general/codebuddy.json +33 -21
- package/harness/manifests/general/codex.json +33 -21
- package/harness/manifests/general/cursor.json +33 -21
- package/harness/manifests/java/claude-code.json +33 -21
- package/harness/manifests/java/codebuddy.json +33 -21
- package/harness/manifests/java/codex.json +33 -21
- package/harness/manifests/java/cursor.json +33 -21
- package/hunter-workflow-family.json +1 -1
- package/package.json +1 -1
|
@@ -24,7 +24,7 @@ import tempfile
|
|
|
24
24
|
import time
|
|
25
25
|
import uuid
|
|
26
26
|
from pathlib import Path
|
|
27
|
-
from typing import Any
|
|
27
|
+
from typing import Any, Sequence
|
|
28
28
|
|
|
29
29
|
SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
30
30
|
if str(SCRIPTS_DIR) not in sys.path:
|
|
@@ -595,6 +595,132 @@ def append_event(
|
|
|
595
595
|
return {"ok": True, "event": event, "events_path": str(events_path_obj), "rendered": False}
|
|
596
596
|
|
|
597
597
|
|
|
598
|
+
def batch_append_events(
|
|
599
|
+
change_dir: Path,
|
|
600
|
+
events: Sequence[dict[str, Any]],
|
|
601
|
+
) -> dict[str, Any]:
|
|
602
|
+
"""Validate then append many events under one lock (Wave-2 H-15).
|
|
603
|
+
|
|
604
|
+
Any invalid item aborts the whole batch before mutation. ``phase.end`` items
|
|
605
|
+
are rejected here — use single ``append`` so render semantics stay explicit.
|
|
606
|
+
"""
|
|
607
|
+
if not isinstance(events, (list, tuple)) or not events:
|
|
608
|
+
return {
|
|
609
|
+
"ok": False,
|
|
610
|
+
"code": "BATCH_EMPTY",
|
|
611
|
+
"message": "batch-append requires a non-empty events array",
|
|
612
|
+
}
|
|
613
|
+
archived = archived_change_dir(change_dir)
|
|
614
|
+
if archived is not None:
|
|
615
|
+
return {
|
|
616
|
+
"ok": False,
|
|
617
|
+
"code": "ARCHIVED_CHANGE_IMMUTABLE",
|
|
618
|
+
"message": str(archived),
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
built: list[dict[str, Any]] = []
|
|
622
|
+
for index, raw in enumerate(events):
|
|
623
|
+
if not isinstance(raw, dict):
|
|
624
|
+
return {
|
|
625
|
+
"ok": False,
|
|
626
|
+
"code": "BATCH_ITEM_INVALID",
|
|
627
|
+
"message": f"events[{index}] must be an object",
|
|
628
|
+
"index": index,
|
|
629
|
+
}
|
|
630
|
+
type_ = str(raw.get("type") or "").strip()
|
|
631
|
+
phase = str(raw.get("phase") or "").strip()
|
|
632
|
+
if not type_ or type_ not in EVENT_TYPES:
|
|
633
|
+
return {
|
|
634
|
+
"ok": False,
|
|
635
|
+
"code": "EVENT_TYPE_INVALID",
|
|
636
|
+
"message": f"events[{index}]: unsupported type: {type_}",
|
|
637
|
+
"index": index,
|
|
638
|
+
}
|
|
639
|
+
if not phase:
|
|
640
|
+
return {
|
|
641
|
+
"ok": False,
|
|
642
|
+
"code": "EVENT_REQUIRED_FIELD",
|
|
643
|
+
"message": f"events[{index}]: phase required",
|
|
644
|
+
"index": index,
|
|
645
|
+
}
|
|
646
|
+
if type_ == "phase.end":
|
|
647
|
+
return {
|
|
648
|
+
"ok": False,
|
|
649
|
+
"code": "BATCH_PHASE_END_FORBIDDEN",
|
|
650
|
+
"message": f"events[{index}]: phase.end must use single append",
|
|
651
|
+
"index": index,
|
|
652
|
+
}
|
|
653
|
+
args = argparse.Namespace(
|
|
654
|
+
change_dir=str(change_dir),
|
|
655
|
+
phase=phase,
|
|
656
|
+
type=type_,
|
|
657
|
+
note=raw.get("note"),
|
|
658
|
+
kind=raw.get("kind"),
|
|
659
|
+
path=raw.get("path"),
|
|
660
|
+
run_id=raw.get("run_id"),
|
|
661
|
+
executor_tool=raw.get("executor_tool"),
|
|
662
|
+
command=raw.get("command"),
|
|
663
|
+
exit_code=raw.get("exit_code"),
|
|
664
|
+
duration_ms=raw.get("duration_ms"),
|
|
665
|
+
status=raw.get("status"),
|
|
666
|
+
name=raw.get("name"),
|
|
667
|
+
code=raw.get("code"),
|
|
668
|
+
severity=raw.get("severity"),
|
|
669
|
+
message=raw.get("message"),
|
|
670
|
+
decision=raw.get("decision"),
|
|
671
|
+
reason=raw.get("reason"),
|
|
672
|
+
issue_id=raw.get("issue_id"),
|
|
673
|
+
scope=raw.get("scope"),
|
|
674
|
+
target_event_id=raw.get("target_event_id"),
|
|
675
|
+
target_field=raw.get("target_field"),
|
|
676
|
+
old_value_hash=raw.get("old_value_hash"),
|
|
677
|
+
new_value_json=raw.get("new_value_json"),
|
|
678
|
+
renamed_from=raw.get("renamed_from"),
|
|
679
|
+
renamed_to=raw.get("renamed_to"),
|
|
680
|
+
change_uuid=raw.get("change_uuid"),
|
|
681
|
+
attempt=raw.get("attempt"),
|
|
682
|
+
executor_agent=raw.get("executor_agent"),
|
|
683
|
+
executor_model=raw.get("executor_model"),
|
|
684
|
+
handoff_from_tool=raw.get("handoff_from_tool"),
|
|
685
|
+
handoff_reason=raw.get("handoff_reason"),
|
|
686
|
+
trace_id=raw.get("trace_id"),
|
|
687
|
+
span_id=raw.get("span_id"),
|
|
688
|
+
parent_span_id=raw.get("parent_span_id"),
|
|
689
|
+
runner_ms=raw.get("runner_ms"),
|
|
690
|
+
orchestration_active_ms=raw.get("orchestration_active_ms"),
|
|
691
|
+
wall_clock_ms=raw.get("wall_clock_ms"),
|
|
692
|
+
user_wait_ms=raw.get("user_wait_ms"),
|
|
693
|
+
legacy_lenient=False,
|
|
694
|
+
json=True,
|
|
695
|
+
)
|
|
696
|
+
validation = validate_append_event(args)
|
|
697
|
+
if validation:
|
|
698
|
+
error_code, message = validation
|
|
699
|
+
return {
|
|
700
|
+
"ok": False,
|
|
701
|
+
"code": error_code,
|
|
702
|
+
"message": f"events[{index}]: {message}",
|
|
703
|
+
"index": index,
|
|
704
|
+
}
|
|
705
|
+
built.append(build_event(args, []))
|
|
706
|
+
|
|
707
|
+
events_path_obj = events_path(change_dir)
|
|
708
|
+
lock_path = events_path_obj.with_name(events_path_obj.name + ".lock")
|
|
709
|
+
with event_file_lock(lock_path):
|
|
710
|
+
for event in built:
|
|
711
|
+
atomic_append_line(
|
|
712
|
+
events_path_obj, json.dumps(event, ensure_ascii=False, separators=(",", ":"))
|
|
713
|
+
)
|
|
714
|
+
return {
|
|
715
|
+
"ok": True,
|
|
716
|
+
"action": "batch-append",
|
|
717
|
+
"count": len(built),
|
|
718
|
+
"events": built,
|
|
719
|
+
"events_path": str(events_path_obj),
|
|
720
|
+
"rendered": False,
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
|
|
598
724
|
def build_event(args: argparse.Namespace, existing: list[dict[str, Any]]) -> dict[str, Any]:
|
|
599
725
|
event: dict[str, Any] = {
|
|
600
726
|
"schema_version": SCHEMA_VERSION,
|
|
@@ -842,28 +968,89 @@ def late_event_stats(phase_events: list[dict[str, Any]]) -> dict[str, int]:
|
|
|
842
968
|
return {"lateEventCount": len(late_stamps), "lateEventSpanMs": span}
|
|
843
969
|
|
|
844
970
|
|
|
845
|
-
def
|
|
846
|
-
|
|
971
|
+
def _seal_timestamp_for_attempt(
|
|
972
|
+
events_in: list[dict[str, Any]],
|
|
973
|
+
*,
|
|
974
|
+
cutoff_ts: str | None,
|
|
975
|
+
next_attempt_start: Any = None,
|
|
976
|
+
) -> tuple[Any, str | None]:
|
|
977
|
+
"""Pick INCOMPLETE seal time: next start, recovery, or report cutoff."""
|
|
978
|
+
recovery_ts = None
|
|
979
|
+
for event in events_in:
|
|
980
|
+
if str(event.get("type") or "").lower() in {
|
|
981
|
+
"recovery",
|
|
982
|
+
"phase.recovery",
|
|
983
|
+
"attempt.recovery",
|
|
984
|
+
}:
|
|
985
|
+
recovery_ts = event.get("timestamp") or recovery_ts
|
|
986
|
+
if next_attempt_start:
|
|
987
|
+
return next_attempt_start, "INCOMPLETE"
|
|
988
|
+
if recovery_ts:
|
|
989
|
+
return recovery_ts, "INCOMPLETE"
|
|
990
|
+
if cutoff_ts:
|
|
991
|
+
return cutoff_ts, "INCOMPLETE"
|
|
992
|
+
# Fall back to last event timestamp so wall clock is not silently dropped.
|
|
993
|
+
for event in reversed(events_in):
|
|
994
|
+
if event.get("timestamp"):
|
|
995
|
+
return event.get("timestamp"), "INCOMPLETE"
|
|
996
|
+
return None, "INCOMPLETE"
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
def attempt_invocations(
|
|
1000
|
+
phase_events: list[dict[str, Any]],
|
|
1001
|
+
*,
|
|
1002
|
+
cutoff_ts: str | None = None,
|
|
1003
|
+
) -> list[dict[str, Any]]:
|
|
1004
|
+
"""Per-attempt invocation view: attempt, status, durationMs (RET-22 / IA-2).
|
|
1005
|
+
|
|
1006
|
+
Closed attempts keep phase.end status. Unclosed attempts are sealed as
|
|
1007
|
+
INCOMPLETE at the next attempt start, an explicit recovery event, or cutoff.
|
|
1008
|
+
"""
|
|
1009
|
+
attempts = split_phase_attempts(phase_events)
|
|
847
1010
|
invocations: list[dict[str, Any]] = []
|
|
848
|
-
for attempt in
|
|
1011
|
+
for index, attempt in enumerate(attempts):
|
|
849
1012
|
events_in = attempt.get("events") or []
|
|
850
1013
|
start_ts = None
|
|
851
1014
|
end_ts = None
|
|
852
1015
|
status = None
|
|
1016
|
+
closed_by_end = False
|
|
853
1017
|
for event in events_in:
|
|
854
1018
|
if event.get("type") == "phase.start" and start_ts is None:
|
|
855
1019
|
start_ts = event.get("timestamp")
|
|
856
1020
|
elif event.get("type") == "phase.end":
|
|
857
1021
|
end_ts = event.get("timestamp")
|
|
858
|
-
|
|
1022
|
+
if "status" in event:
|
|
1023
|
+
status = event.get("status")
|
|
1024
|
+
closed_by_end = True
|
|
1025
|
+
sealed_incomplete = False
|
|
1026
|
+
if not closed_by_end:
|
|
1027
|
+
next_start = None
|
|
1028
|
+
if index + 1 < len(attempts):
|
|
1029
|
+
for event in attempts[index + 1].get("events") or []:
|
|
1030
|
+
if event.get("type") == "phase.start" and event.get("timestamp"):
|
|
1031
|
+
next_start = event.get("timestamp")
|
|
1032
|
+
break
|
|
1033
|
+
seal_ts, seal_status = _seal_timestamp_for_attempt(
|
|
1034
|
+
events_in,
|
|
1035
|
+
cutoff_ts=cutoff_ts,
|
|
1036
|
+
next_attempt_start=next_start,
|
|
1037
|
+
)
|
|
1038
|
+
if seal_ts is not None:
|
|
1039
|
+
end_ts = seal_ts
|
|
1040
|
+
status = seal_status
|
|
1041
|
+
sealed_incomplete = True
|
|
859
1042
|
duration = None
|
|
860
1043
|
if start_ts and end_ts:
|
|
861
1044
|
duration = duration_ms_between(start_ts, end_ts)
|
|
1045
|
+
# Active execution counts phase.end-closed attempts only (INCOMPLETE seals excluded).
|
|
1046
|
+
active_eligible = closed_by_end and not sealed_incomplete
|
|
862
1047
|
invocations.append(
|
|
863
1048
|
{
|
|
864
1049
|
"attempt": attempt.get("attempt"),
|
|
865
1050
|
"status": status,
|
|
866
1051
|
"durationMs": duration,
|
|
1052
|
+
"activeEligible": active_eligible,
|
|
1053
|
+
"sealedIncomplete": sealed_incomplete,
|
|
867
1054
|
}
|
|
868
1055
|
)
|
|
869
1056
|
return invocations
|
|
@@ -879,30 +1066,48 @@ def phase_final_state(phase_events: list[dict[str, Any]]) -> dict[str, Any]:
|
|
|
879
1066
|
"attempt": latest.get("attempt"),
|
|
880
1067
|
"status": latest.get("status"),
|
|
881
1068
|
"durationMs": latest.get("durationMs"),
|
|
882
|
-
"closed": latest.get("status") is not None,
|
|
1069
|
+
"closed": latest.get("status") is not None and latest.get("status") != "INCOMPLETE",
|
|
883
1070
|
}
|
|
884
1071
|
|
|
885
1072
|
|
|
886
|
-
def canonical_phase_timing(
|
|
887
|
-
|
|
1073
|
+
def canonical_phase_timing(
|
|
1074
|
+
phase_events: list[dict[str, Any]],
|
|
1075
|
+
*,
|
|
1076
|
+
cutoff_ts: str | None = None,
|
|
1077
|
+
) -> dict[str, Any]:
|
|
1078
|
+
"""Single reducer for every duration view (RET-20 / IA-2).
|
|
888
1079
|
|
|
889
|
-
activeExecutionMs:
|
|
890
|
-
wallClockSpanMs: first → last timestamp
|
|
1080
|
+
activeExecutionMs: sum of closed attempt start→end only (INCOMPLETE excluded).
|
|
1081
|
+
wallClockSpanMs: first → last/cutoff timestamp; unclosed attempts retain span.
|
|
891
1082
|
"""
|
|
1083
|
+
invocations = attempt_invocations(phase_events, cutoff_ts=cutoff_ts)
|
|
892
1084
|
stamps = [e.get("timestamp") for e in phase_events if e.get("timestamp")]
|
|
893
|
-
|
|
1085
|
+
# Cutoff seals INCOMPLETE attempts but must not inflate stage wall by itself.
|
|
1086
|
+
# Include cutoff in the wall span only when it sealed an otherwise-open attempt.
|
|
1087
|
+
if cutoff_ts and any(inv.get("sealedIncomplete") for inv in invocations):
|
|
1088
|
+
stamps = list(stamps) + [cutoff_ts]
|
|
1089
|
+
closed_durations = [
|
|
894
1090
|
invocation["durationMs"]
|
|
895
|
-
for invocation in
|
|
896
|
-
if invocation.get("durationMs") is not None
|
|
1091
|
+
for invocation in invocations
|
|
1092
|
+
if invocation.get("activeEligible") and invocation.get("durationMs") is not None
|
|
897
1093
|
]
|
|
898
|
-
active = sum(
|
|
1094
|
+
active = sum(closed_durations) if closed_durations else (0 if invocations else None)
|
|
899
1095
|
late = late_event_stats(phase_events)
|
|
900
|
-
|
|
1096
|
+
if len(stamps) >= 2:
|
|
1097
|
+
wall = duration_ms_between(stamps[0], stamps[-1])
|
|
1098
|
+
else:
|
|
1099
|
+
wall = active
|
|
1100
|
+
unclosed = sum(
|
|
1101
|
+
1
|
|
1102
|
+
for invocation in invocations
|
|
1103
|
+
if invocation.get("sealedIncomplete") or invocation.get("status") == "INCOMPLETE"
|
|
1104
|
+
)
|
|
901
1105
|
return {
|
|
902
1106
|
"activeExecutionMs": active,
|
|
903
1107
|
"wallClockSpanMs": wall,
|
|
904
1108
|
"lateEventCount": late["lateEventCount"],
|
|
905
1109
|
"lateEventSpanMs": late["lateEventSpanMs"],
|
|
1110
|
+
"unclosedAttemptCount": unclosed,
|
|
906
1111
|
}
|
|
907
1112
|
|
|
908
1113
|
|
|
@@ -1295,6 +1500,25 @@ def cmd_append(args: argparse.Namespace) -> int:
|
|
|
1295
1500
|
return 0
|
|
1296
1501
|
|
|
1297
1502
|
|
|
1503
|
+
def cmd_batch_append(args: argparse.Namespace) -> int:
|
|
1504
|
+
as_json = bool(args.json)
|
|
1505
|
+
change_dir = resolve_change_dir(args.change_dir)
|
|
1506
|
+
batch_path = Path(str(args.file)).expanduser()
|
|
1507
|
+
try:
|
|
1508
|
+
raw = json.loads(batch_path.read_text(encoding="utf-8-sig"))
|
|
1509
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
1510
|
+
return emit_error(f"batch-append failed to read --file: {exc}", as_json=as_json)
|
|
1511
|
+
result = batch_append_events(change_dir, raw if isinstance(raw, list) else [])
|
|
1512
|
+
if not result.get("ok"):
|
|
1513
|
+
return emit_error(
|
|
1514
|
+
str(result.get("message") or "batch-append failed"),
|
|
1515
|
+
as_json=as_json,
|
|
1516
|
+
error_code=str(result.get("code") or "BATCH_APPEND_FAILED"),
|
|
1517
|
+
)
|
|
1518
|
+
emit_json(result, as_json=as_json)
|
|
1519
|
+
return 0
|
|
1520
|
+
|
|
1521
|
+
|
|
1298
1522
|
def cmd_render(args: argparse.Namespace) -> int:
|
|
1299
1523
|
as_json = bool(args.json)
|
|
1300
1524
|
change_dir = resolve_change_dir(args.change_dir)
|
|
@@ -1395,6 +1619,19 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1395
1619
|
)
|
|
1396
1620
|
p_append.set_defaults(func=cmd_append)
|
|
1397
1621
|
|
|
1622
|
+
p_batch = sub.add_parser(
|
|
1623
|
+
"batch-append",
|
|
1624
|
+
parents=[common],
|
|
1625
|
+
help="validate and append many events under one lock (H-15)",
|
|
1626
|
+
)
|
|
1627
|
+
p_batch.add_argument("--change-dir", required=True)
|
|
1628
|
+
p_batch.add_argument(
|
|
1629
|
+
"--file",
|
|
1630
|
+
required=True,
|
|
1631
|
+
help="JSON file containing a non-empty array of event objects",
|
|
1632
|
+
)
|
|
1633
|
+
p_batch.set_defaults(func=cmd_batch_append)
|
|
1634
|
+
|
|
1398
1635
|
p_render = sub.add_parser(
|
|
1399
1636
|
"render",
|
|
1400
1637
|
parents=[common],
|
|
@@ -862,8 +862,42 @@ class IntegrationTransaction:
|
|
|
862
862
|
|
|
863
863
|
return self._run_step("merge", action)
|
|
864
864
|
|
|
865
|
+
def _required_on_merge_commands(self) -> list[list[str]]:
|
|
866
|
+
"""Load build-profile mergeVerification.requiredOnMerge (Wave-2 H-9)."""
|
|
867
|
+
import harness_profile
|
|
868
|
+
|
|
869
|
+
profile = harness_profile.load_profile(self.project_root)
|
|
870
|
+
if not isinstance(profile, dict):
|
|
871
|
+
return []
|
|
872
|
+
merge = profile.get("mergeVerification")
|
|
873
|
+
if not isinstance(merge, dict):
|
|
874
|
+
return []
|
|
875
|
+
required = merge.get("requiredOnMerge")
|
|
876
|
+
if not isinstance(required, list):
|
|
877
|
+
return []
|
|
878
|
+
commands: list[list[str]] = []
|
|
879
|
+
for index, item in enumerate(required):
|
|
880
|
+
if isinstance(item, str) and item.strip():
|
|
881
|
+
commands.append(shlex.split(item.strip()))
|
|
882
|
+
continue
|
|
883
|
+
if not isinstance(item, dict):
|
|
884
|
+
raise ValueError(f"requiredOnMerge[{index}] must be string or object")
|
|
885
|
+
raw = item.get("command")
|
|
886
|
+
if isinstance(raw, list) and raw and all(isinstance(x, str) and x for x in raw):
|
|
887
|
+
commands.append([str(x) for x in raw])
|
|
888
|
+
elif isinstance(raw, str) and raw.strip():
|
|
889
|
+
commands.append(shlex.split(raw.strip()))
|
|
890
|
+
else:
|
|
891
|
+
raise ValueError(f"requiredOnMerge[{index}].command missing")
|
|
892
|
+
return commands
|
|
893
|
+
|
|
865
894
|
def verify(self, commands: Sequence[Sequence[str]] | None = None) -> dict[str, Any]:
|
|
866
895
|
commands = list(commands or [])
|
|
896
|
+
# Wave-2 H-9: append profile mergeVerification.requiredOnMerge commands.
|
|
897
|
+
try:
|
|
898
|
+
commands.extend(self._required_on_merge_commands())
|
|
899
|
+
except Exception as exc: # noqa: BLE001 — surface as verify plan error
|
|
900
|
+
raise VerifyPlanMissingError(f"mergeVerification load failed: {exc}") from exc
|
|
867
901
|
|
|
868
902
|
def action(journal: dict[str, Any]) -> None:
|
|
869
903
|
if not commands or any(
|
|
@@ -1055,6 +1055,7 @@ def decide_can_reuse(
|
|
|
1055
1055
|
requested_toolchain_hash: str | None = None,
|
|
1056
1056
|
requested_profile_hash: str | None = None,
|
|
1057
1057
|
requested_environment_hash: str | None = None,
|
|
1058
|
+
requested_db_schema_hash: str | None = None,
|
|
1058
1059
|
) -> dict[str, Any]:
|
|
1059
1060
|
ledger, ledger_path = load_ledger(change_dir)
|
|
1060
1061
|
if ledger is None:
|
|
@@ -1183,6 +1184,7 @@ def decide_can_reuse(
|
|
|
1183
1184
|
("toolchainHash", requested_toolchain_hash, "TOOLCHAIN_CHANGED"),
|
|
1184
1185
|
("profileHash", requested_profile_hash, "PROFILE_CHANGED"),
|
|
1185
1186
|
("environmentHash", requested_environment_hash, "ENVIRONMENT_CHANGED"),
|
|
1187
|
+
("dbSchemaHash", requested_db_schema_hash, "DB_SCHEMA_CHANGED"),
|
|
1186
1188
|
):
|
|
1187
1189
|
stored = entry.get(field)
|
|
1188
1190
|
if (
|
|
@@ -1249,6 +1251,7 @@ def decide_can_reuse(
|
|
|
1249
1251
|
"reuse": True,
|
|
1250
1252
|
"reason": "reuse",
|
|
1251
1253
|
"code": "REUSED",
|
|
1254
|
+
"invalidationCode": None,
|
|
1252
1255
|
"verification": verification,
|
|
1253
1256
|
"ledger_path": str(ledger_path) if ledger_path else None,
|
|
1254
1257
|
"inputsHash": stored_hash,
|
|
@@ -1258,6 +1261,90 @@ def decide_can_reuse(
|
|
|
1258
1261
|
}
|
|
1259
1262
|
|
|
1260
1263
|
|
|
1264
|
+
_INVALIDATION_ALIASES = {
|
|
1265
|
+
"INPUTS_HASH_CHANGED": "INPUT_CHANGED",
|
|
1266
|
+
"ENVIRONMENT_CHANGED": "ENV_CHANGED",
|
|
1267
|
+
"MISSING_FIELDS": "MISSING_IDENTITY_FIELD",
|
|
1268
|
+
"MISSING_V2_FIELDS": "MISSING_IDENTITY_FIELD",
|
|
1269
|
+
"LEDGER_MISSING": "MISSING_IDENTITY_FIELD",
|
|
1270
|
+
"VALIDATIONS_MISSING": "MISSING_IDENTITY_FIELD",
|
|
1271
|
+
"VALIDATION_MISSING": "MISSING_IDENTITY_FIELD",
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
|
|
1275
|
+
def _attach_invalidation_code(payload: dict[str, Any]) -> dict[str, Any]:
|
|
1276
|
+
"""Wave-2 H-9: expose stable invalidationCode alongside legacy code."""
|
|
1277
|
+
if payload.get("reuse") is True:
|
|
1278
|
+
payload.setdefault("invalidationCode", None)
|
|
1279
|
+
return payload
|
|
1280
|
+
code = str(payload.get("code") or "").strip()
|
|
1281
|
+
if code:
|
|
1282
|
+
payload["invalidationCode"] = _INVALIDATION_ALIASES.get(code, code)
|
|
1283
|
+
return payload
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
def verification_graph_path(change_dir: Path) -> Path:
|
|
1287
|
+
return Path(change_dir) / "evidence" / "verification-graph.json"
|
|
1288
|
+
|
|
1289
|
+
|
|
1290
|
+
def upsert_verification_graph_node(
|
|
1291
|
+
change_dir: Path,
|
|
1292
|
+
*,
|
|
1293
|
+
verification: str,
|
|
1294
|
+
status: str,
|
|
1295
|
+
inputs_hash: str,
|
|
1296
|
+
command: str | None = None,
|
|
1297
|
+
toolchain_hash: str | None = None,
|
|
1298
|
+
environment_hash: str | None = None,
|
|
1299
|
+
db_schema_hash: str | None = None,
|
|
1300
|
+
diff_hash: str | None = None,
|
|
1301
|
+
) -> dict[str, Any]:
|
|
1302
|
+
"""Persist a verification graph node keyed by canonical identity (H-9)."""
|
|
1303
|
+
path = verification_graph_path(change_dir)
|
|
1304
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1305
|
+
graph: dict[str, Any]
|
|
1306
|
+
if path.is_file():
|
|
1307
|
+
try:
|
|
1308
|
+
loaded = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
1309
|
+
graph = loaded if isinstance(loaded, dict) else {"schemaVersion": 1, "nodes": []}
|
|
1310
|
+
except (OSError, json.JSONDecodeError):
|
|
1311
|
+
graph = {"schemaVersion": 1, "nodes": []}
|
|
1312
|
+
else:
|
|
1313
|
+
graph = {"schemaVersion": 1, "nodes": []}
|
|
1314
|
+
nodes = graph.get("nodes")
|
|
1315
|
+
if not isinstance(nodes, list):
|
|
1316
|
+
nodes = []
|
|
1317
|
+
identity = {
|
|
1318
|
+
"verification": verification,
|
|
1319
|
+
"inputsHash": inputs_hash,
|
|
1320
|
+
"command": command,
|
|
1321
|
+
"toolchainHash": toolchain_hash,
|
|
1322
|
+
"environmentHash": environment_hash,
|
|
1323
|
+
"dbSchemaHash": db_schema_hash,
|
|
1324
|
+
"diffHash": diff_hash,
|
|
1325
|
+
}
|
|
1326
|
+
node = {
|
|
1327
|
+
"identity": identity,
|
|
1328
|
+
"status": status,
|
|
1329
|
+
"recordedAt": dt.datetime.now().astimezone().isoformat(timespec="milliseconds"),
|
|
1330
|
+
}
|
|
1331
|
+
replaced = False
|
|
1332
|
+
for index, existing in enumerate(nodes):
|
|
1333
|
+
if not isinstance(existing, dict):
|
|
1334
|
+
continue
|
|
1335
|
+
existing_id = existing.get("identity")
|
|
1336
|
+
if isinstance(existing_id, dict) and existing_id == identity:
|
|
1337
|
+
nodes[index] = node
|
|
1338
|
+
replaced = True
|
|
1339
|
+
break
|
|
1340
|
+
if not replaced:
|
|
1341
|
+
nodes.append(node)
|
|
1342
|
+
graph["schemaVersion"] = 1
|
|
1343
|
+
graph["nodes"] = nodes
|
|
1344
|
+
write_ledger(path, graph) # atomic json write helper already used for ledger
|
|
1345
|
+
return {"ok": True, "path": str(path), "nodeCount": len(nodes), "replaced": replaced}
|
|
1346
|
+
|
|
1347
|
+
|
|
1261
1348
|
def cmd_hash(args: argparse.Namespace) -> int:
|
|
1262
1349
|
as_json = bool(args.json)
|
|
1263
1350
|
try:
|
|
@@ -1330,10 +1417,12 @@ def cmd_can_reuse(args: argparse.Namespace) -> int:
|
|
|
1330
1417
|
requested_toolchain_hash=getattr(args, "toolchain_hash", None),
|
|
1331
1418
|
requested_profile_hash=getattr(args, "profile_hash", None),
|
|
1332
1419
|
requested_environment_hash=getattr(args, "environment_hash", None),
|
|
1420
|
+
requested_db_schema_hash=getattr(args, "db_schema_hash", None),
|
|
1333
1421
|
)
|
|
1334
1422
|
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
1335
1423
|
return emit_error(f"can-reuse failed: {exc}", as_json=as_json)
|
|
1336
1424
|
|
|
1425
|
+
payload = _attach_invalidation_code(payload)
|
|
1337
1426
|
emit_compact_or_verbose(
|
|
1338
1427
|
payload, as_json=as_json, verbose=verbose,
|
|
1339
1428
|
compact_fn=_compact_can_reuse_payload,
|
|
@@ -1470,6 +1559,7 @@ def cmd_record(args: argparse.Namespace) -> int:
|
|
|
1470
1559
|
("toolchainHash", "toolchain_hash"),
|
|
1471
1560
|
("profileHash", "profile_hash"),
|
|
1472
1561
|
("environmentHash", "environment_hash"),
|
|
1562
|
+
("dbSchemaHash", "db_schema_hash"),
|
|
1473
1563
|
):
|
|
1474
1564
|
val = getattr(args, attr, None)
|
|
1475
1565
|
if _nonempty_str(val):
|
|
@@ -1548,6 +1638,37 @@ def cmd_record(args: argparse.Namespace) -> int:
|
|
|
1548
1638
|
extra={"missing": missing},
|
|
1549
1639
|
)
|
|
1550
1640
|
write_ledger(out_path, ledger)
|
|
1641
|
+
try:
|
|
1642
|
+
upsert_verification_graph_node(
|
|
1643
|
+
change_dir,
|
|
1644
|
+
verification=verification,
|
|
1645
|
+
status=str(status),
|
|
1646
|
+
inputs_hash=str(inputs_hash),
|
|
1647
|
+
command=str(entry.get("command") or "") or None,
|
|
1648
|
+
toolchain_hash=(
|
|
1649
|
+
str(entry.get("toolchainHash")).strip()
|
|
1650
|
+
if _nonempty_str(entry.get("toolchainHash"))
|
|
1651
|
+
else None
|
|
1652
|
+
),
|
|
1653
|
+
environment_hash=(
|
|
1654
|
+
str(entry.get("environmentHash")).strip()
|
|
1655
|
+
if _nonempty_str(entry.get("environmentHash"))
|
|
1656
|
+
else None
|
|
1657
|
+
),
|
|
1658
|
+
db_schema_hash=(
|
|
1659
|
+
str(entry.get("dbSchemaHash")).strip()
|
|
1660
|
+
if _nonempty_str(entry.get("dbSchemaHash"))
|
|
1661
|
+
else None
|
|
1662
|
+
),
|
|
1663
|
+
diff_hash=(
|
|
1664
|
+
str(ledger.get("diffHash")).strip()
|
|
1665
|
+
if _nonempty_str(ledger.get("diffHash"))
|
|
1666
|
+
else None
|
|
1667
|
+
),
|
|
1668
|
+
)
|
|
1669
|
+
except (OSError, ValueError, TypeError):
|
|
1670
|
+
# Graph is advisory persistence; ledger write already succeeded.
|
|
1671
|
+
pass
|
|
1551
1672
|
except (OSError, ValueError, FileNotFoundError, json.JSONDecodeError) as exc:
|
|
1552
1673
|
return emit_error(f"record failed: {exc}", as_json=as_json)
|
|
1553
1674
|
|
|
@@ -1667,6 +1788,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1667
1788
|
default=None,
|
|
1668
1789
|
help="optional environment hash to compare against ledger entry (UT-017)",
|
|
1669
1790
|
)
|
|
1791
|
+
p_reuse.add_argument(
|
|
1792
|
+
"--db-schema-hash",
|
|
1793
|
+
default=None,
|
|
1794
|
+
help="optional DB schema hash to compare against ledger entry (H-9)",
|
|
1795
|
+
)
|
|
1670
1796
|
p_reuse.add_argument(
|
|
1671
1797
|
"--verbose",
|
|
1672
1798
|
action="store_true",
|
|
@@ -1708,6 +1834,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1708
1834
|
p_record.add_argument("--toolchain-hash", default=None)
|
|
1709
1835
|
p_record.add_argument("--profile-hash", default=None)
|
|
1710
1836
|
p_record.add_argument("--environment-hash", default=None)
|
|
1837
|
+
p_record.add_argument("--db-schema-hash", default=None)
|
|
1711
1838
|
p_record.add_argument("--deploy-artifact", default=None, help="package: built artifact path")
|
|
1712
1839
|
p_record.add_argument("--artifact-hash", default=None, help="package: artifact sha256")
|
|
1713
1840
|
p_record.add_argument(
|