@hunter-harness/workflow-harness 0.2.26 → 0.2.28
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 +1 -1
- package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +25 -6
- package/harness/bundles/general/claude-code/harness-archive/templates/summary-data-template.json +44 -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-ingest/scripts/harness_knowledge.py +493 -74
- 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-submit/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +8 -4
- package/harness/bundles/general/claude-code/harness-sync/reference.md +28 -0
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +2147 -189
- package/harness/bundles/general/claude-code/scripts/harness_change.py +8 -3
- package/harness/bundles/general/claude-code/scripts/harness_events.py +57 -9
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +530 -8
- package/harness/bundles/general/claude-code/scripts/harness_integration.py +263 -6
- package/harness/bundles/general/claude-code/scripts/harness_phase.py +758 -1
- package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +74 -0
- package/harness/bundles/general/claude-code/scripts/harness_retry.py +175 -0
- package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +702 -22
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +25 -6
- package/harness/bundles/general/codebuddy/harness-archive/templates/summary-data-template.json +44 -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-ingest/scripts/harness_knowledge.py +493 -74
- 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-submit/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +8 -4
- package/harness/bundles/general/codebuddy/harness-sync/reference.md +28 -0
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +2147 -189
- package/harness/bundles/general/codebuddy/scripts/harness_change.py +8 -3
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +57 -9
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +530 -8
- package/harness/bundles/general/codebuddy/scripts/harness_integration.py +263 -6
- package/harness/bundles/general/codebuddy/scripts/harness_phase.py +758 -1
- package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +74 -0
- package/harness/bundles/general/codebuddy/scripts/harness_retry.py +175 -0
- package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +702 -22
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +25 -6
- package/harness/bundles/general/codex/harness-archive/templates/summary-data-template.json +44 -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-ingest/scripts/harness_knowledge.py +493 -74
- 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-submit/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-sync/SKILL.md +8 -4
- package/harness/bundles/general/codex/harness-sync/reference.md +28 -0
- package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codex/scripts/harness_archive.py +2147 -189
- package/harness/bundles/general/codex/scripts/harness_change.py +8 -3
- package/harness/bundles/general/codex/scripts/harness_events.py +57 -9
- package/harness/bundles/general/codex/scripts/harness_gate.py +530 -8
- package/harness/bundles/general/codex/scripts/harness_integration.py +263 -6
- package/harness/bundles/general/codex/scripts/harness_phase.py +758 -1
- package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +74 -0
- package/harness/bundles/general/codex/scripts/harness_retry.py +175 -0
- package/harness/bundles/general/codex/scripts/harness_test_guard.py +702 -22
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +25 -6
- package/harness/bundles/general/cursor/harness-archive/templates/summary-data-template.json +44 -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-ingest/scripts/harness_knowledge.py +493 -74
- 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-submit/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +8 -4
- package/harness/bundles/general/cursor/harness-sync/reference.md +28 -0
- package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/cursor/scripts/harness_archive.py +2147 -189
- package/harness/bundles/general/cursor/scripts/harness_change.py +8 -3
- package/harness/bundles/general/cursor/scripts/harness_events.py +57 -9
- package/harness/bundles/general/cursor/scripts/harness_gate.py +530 -8
- package/harness/bundles/general/cursor/scripts/harness_integration.py +263 -6
- package/harness/bundles/general/cursor/scripts/harness_phase.py +758 -1
- package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +74 -0
- package/harness/bundles/general/cursor/scripts/harness_retry.py +175 -0
- package/harness/bundles/general/cursor/scripts/harness_test_guard.py +702 -22
- 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 +1 -1
- package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +25 -6
- package/harness/bundles/java/claude-code/harness-archive/templates/summary-data-template.json +44 -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-ingest/scripts/harness_knowledge.py +493 -74
- 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-sync/SKILL.md +8 -4
- package/harness/bundles/java/claude-code/harness-sync/reference.md +28 -0
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +2147 -189
- package/harness/bundles/java/claude-code/scripts/harness_change.py +8 -3
- package/harness/bundles/java/claude-code/scripts/harness_events.py +57 -9
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +530 -8
- package/harness/bundles/java/claude-code/scripts/harness_integration.py +263 -6
- package/harness/bundles/java/claude-code/scripts/harness_phase.py +758 -1
- package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +74 -0
- package/harness/bundles/java/claude-code/scripts/harness_retry.py +175 -0
- package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +702 -22
- 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 +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +25 -6
- package/harness/bundles/java/codebuddy/harness-archive/templates/summary-data-template.json +44 -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-ingest/scripts/harness_knowledge.py +493 -74
- 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-sync/SKILL.md +8 -4
- package/harness/bundles/java/codebuddy/harness-sync/reference.md +28 -0
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +2147 -189
- package/harness/bundles/java/codebuddy/scripts/harness_change.py +8 -3
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +57 -9
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +530 -8
- package/harness/bundles/java/codebuddy/scripts/harness_integration.py +263 -6
- package/harness/bundles/java/codebuddy/scripts/harness_phase.py +758 -1
- package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +74 -0
- package/harness/bundles/java/codebuddy/scripts/harness_retry.py +175 -0
- package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +702 -22
- 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 +1 -1
- package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +25 -6
- package/harness/bundles/java/codex/harness-archive/templates/summary-data-template.json +44 -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-ingest/scripts/harness_knowledge.py +493 -74
- 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-sync/SKILL.md +8 -4
- package/harness/bundles/java/codex/harness-sync/reference.md +28 -0
- package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codex/scripts/harness_archive.py +2147 -189
- package/harness/bundles/java/codex/scripts/harness_change.py +8 -3
- package/harness/bundles/java/codex/scripts/harness_events.py +57 -9
- package/harness/bundles/java/codex/scripts/harness_gate.py +530 -8
- package/harness/bundles/java/codex/scripts/harness_integration.py +263 -6
- package/harness/bundles/java/codex/scripts/harness_phase.py +758 -1
- package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +74 -0
- package/harness/bundles/java/codex/scripts/harness_retry.py +175 -0
- package/harness/bundles/java/codex/scripts/harness_test_guard.py +702 -22
- 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 +1 -1
- package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +25 -6
- package/harness/bundles/java/cursor/harness-archive/templates/summary-data-template.json +44 -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-ingest/scripts/harness_knowledge.py +493 -74
- 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-sync/SKILL.md +8 -4
- package/harness/bundles/java/cursor/harness-sync/reference.md +28 -0
- package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/cursor/scripts/harness_archive.py +2147 -189
- package/harness/bundles/java/cursor/scripts/harness_change.py +8 -3
- package/harness/bundles/java/cursor/scripts/harness_events.py +57 -9
- package/harness/bundles/java/cursor/scripts/harness_gate.py +530 -8
- package/harness/bundles/java/cursor/scripts/harness_integration.py +263 -6
- package/harness/bundles/java/cursor/scripts/harness_phase.py +758 -1
- package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +74 -0
- package/harness/bundles/java/cursor/scripts/harness_retry.py +175 -0
- package/harness/bundles/java/cursor/scripts/harness_test_guard.py +702 -22
- package/harness/manifests/general/claude-code.json +28 -24
- package/harness/manifests/general/codebuddy.json +28 -24
- package/harness/manifests/general/codex.json +28 -24
- package/harness/manifests/general/cursor.json +28 -24
- package/harness/manifests/java/claude-code.json +30 -26
- package/harness/manifests/java/codebuddy.json +30 -26
- package/harness/manifests/java/codex.json +30 -26
- package/harness/manifests/java/cursor.json +30 -26
- package/hunter-workflow-family.json +2 -2
- package/package.json +1 -1
|
@@ -42,6 +42,12 @@ VALIDATION_PHASES = {
|
|
|
42
42
|
"dbCompatibility": "test",
|
|
43
43
|
"package": "package",
|
|
44
44
|
}
|
|
45
|
+
FINAL_SEQUENCE_RECEIPTS_REL = (
|
|
46
|
+
Path("evidence") / "final-sequence-receipts.json"
|
|
47
|
+
)
|
|
48
|
+
ENVIRONMENT_EXECUTIONS_REL = (
|
|
49
|
+
Path("evidence") / "environment-executions.json"
|
|
50
|
+
)
|
|
45
51
|
|
|
46
52
|
|
|
47
53
|
def _read_json(path: Path) -> dict[str, Any]:
|
|
@@ -66,6 +72,621 @@ def _write_json(path: Path, value: Any) -> None:
|
|
|
66
72
|
raise
|
|
67
73
|
|
|
68
74
|
|
|
75
|
+
def _final_sequence_contract(change_dir: Path) -> dict[str, Any] | None:
|
|
76
|
+
policy_path = change_dir / "meta" / "gate-policy.json"
|
|
77
|
+
if not policy_path.is_file():
|
|
78
|
+
return None
|
|
79
|
+
policy = _read_json(policy_path)
|
|
80
|
+
declared = policy.get("finalSequence")
|
|
81
|
+
if isinstance(declared, dict) and isinstance(declared.get("nodeIds"), list):
|
|
82
|
+
return declared
|
|
83
|
+
dag = policy.get("requiredGateDag")
|
|
84
|
+
if not isinstance(dag, dict):
|
|
85
|
+
return None
|
|
86
|
+
sequence_nodes = [
|
|
87
|
+
node
|
|
88
|
+
for node in (dag.get("nodes") or [])
|
|
89
|
+
if isinstance(node, dict) and node.get("kind") == "sequence"
|
|
90
|
+
]
|
|
91
|
+
if not sequence_nodes:
|
|
92
|
+
return None
|
|
93
|
+
sequence_nodes.sort(key=lambda node: int(node.get("sequenceIndex") or 0))
|
|
94
|
+
node_ids = [str(node.get("id") or "") for node in sequence_nodes]
|
|
95
|
+
return {
|
|
96
|
+
"schemaVersion": 1,
|
|
97
|
+
"nodeIds": node_ids,
|
|
98
|
+
"freezeNodeId": (
|
|
99
|
+
"sequence:code-freeze"
|
|
100
|
+
if "sequence:code-freeze" in node_ids
|
|
101
|
+
else None
|
|
102
|
+
),
|
|
103
|
+
"terminalNodeId": node_ids[-1],
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _load_sequence_receipts(change_dir: Path) -> dict[str, Any]:
|
|
108
|
+
path = change_dir / FINAL_SEQUENCE_RECEIPTS_REL
|
|
109
|
+
if not path.is_file():
|
|
110
|
+
return {
|
|
111
|
+
"schemaVersion": 1,
|
|
112
|
+
"changeId": change_dir.name,
|
|
113
|
+
"receipts": {},
|
|
114
|
+
"attempts": [],
|
|
115
|
+
}
|
|
116
|
+
value = _read_json(path)
|
|
117
|
+
if value.get("schemaVersion") != 1:
|
|
118
|
+
raise ValueError("unsupported final-sequence receipt schema")
|
|
119
|
+
receipts = value.get("receipts")
|
|
120
|
+
if not isinstance(receipts, dict):
|
|
121
|
+
raise ValueError("final-sequence receipts must be an object")
|
|
122
|
+
attempts = value.get("attempts")
|
|
123
|
+
if attempts is not None and not isinstance(attempts, list):
|
|
124
|
+
raise ValueError("final-sequence attempts must be an array")
|
|
125
|
+
value.setdefault("attempts", [])
|
|
126
|
+
return value
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _validated_sequence_subject(subject: dict[str, Any]) -> dict[str, str]:
|
|
130
|
+
required = ("productCommit", "productTreeHash", "environmentHash")
|
|
131
|
+
missing = [
|
|
132
|
+
field
|
|
133
|
+
for field in required
|
|
134
|
+
if not isinstance(subject.get(field), str)
|
|
135
|
+
or not str(subject[field]).strip()
|
|
136
|
+
]
|
|
137
|
+
if missing:
|
|
138
|
+
raise ValueError(
|
|
139
|
+
"final-sequence subject missing: " + ", ".join(missing)
|
|
140
|
+
)
|
|
141
|
+
return {field: str(subject[field]).strip() for field in required}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _record_sequence_attempt(
|
|
145
|
+
change_dir: Path,
|
|
146
|
+
journal: dict[str, Any],
|
|
147
|
+
*,
|
|
148
|
+
node_id: str,
|
|
149
|
+
ok: bool,
|
|
150
|
+
code: str,
|
|
151
|
+
subject: dict[str, str],
|
|
152
|
+
) -> None:
|
|
153
|
+
attempts = journal.setdefault("attempts", [])
|
|
154
|
+
attempts.append(
|
|
155
|
+
{
|
|
156
|
+
"nodeId": node_id,
|
|
157
|
+
"ok": ok,
|
|
158
|
+
"code": code,
|
|
159
|
+
"subject": subject,
|
|
160
|
+
"recordedAt": he.now_iso(),
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
_write_json(change_dir / FINAL_SEQUENCE_RECEIPTS_REL, journal)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def record_sequence_receipt(
|
|
167
|
+
change_dir: Path,
|
|
168
|
+
node_id: str,
|
|
169
|
+
subject: dict[str, Any],
|
|
170
|
+
*,
|
|
171
|
+
status: str = "OK",
|
|
172
|
+
evidence: dict[str, Any] | None = None,
|
|
173
|
+
) -> dict[str, Any]:
|
|
174
|
+
"""Record one final-sequence transition after validating its predecessor."""
|
|
175
|
+
change_dir = change_dir.resolve()
|
|
176
|
+
contract = _final_sequence_contract(change_dir)
|
|
177
|
+
if contract is None:
|
|
178
|
+
return {
|
|
179
|
+
"ok": False,
|
|
180
|
+
"code": "FINAL_SEQUENCE_NOT_CONFIGURED",
|
|
181
|
+
"message": "gate policy does not declare finalSequence",
|
|
182
|
+
}
|
|
183
|
+
node_ids = [
|
|
184
|
+
str(item)
|
|
185
|
+
for item in contract.get("nodeIds") or []
|
|
186
|
+
if isinstance(item, str)
|
|
187
|
+
]
|
|
188
|
+
if node_id not in node_ids:
|
|
189
|
+
return {
|
|
190
|
+
"ok": False,
|
|
191
|
+
"code": "SEQUENCE_NODE_UNKNOWN",
|
|
192
|
+
"message": f"node is not in finalSequence: {node_id}",
|
|
193
|
+
}
|
|
194
|
+
try:
|
|
195
|
+
normalized_subject = _validated_sequence_subject(subject)
|
|
196
|
+
journal = _load_sequence_receipts(change_dir)
|
|
197
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
198
|
+
return {
|
|
199
|
+
"ok": False,
|
|
200
|
+
"code": "SEQUENCE_RECEIPT_INVALID",
|
|
201
|
+
"message": str(exc),
|
|
202
|
+
}
|
|
203
|
+
receipts = journal["receipts"]
|
|
204
|
+
index = node_ids.index(node_id)
|
|
205
|
+
predecessor_id = node_ids[index - 1] if index else None
|
|
206
|
+
if predecessor_id is not None:
|
|
207
|
+
predecessor = receipts.get(predecessor_id)
|
|
208
|
+
if (
|
|
209
|
+
not isinstance(predecessor, dict)
|
|
210
|
+
or str(predecessor.get("status") or "").upper() != "OK"
|
|
211
|
+
):
|
|
212
|
+
code = "SEQUENCE_PREDECESSOR_MISSING"
|
|
213
|
+
_record_sequence_attempt(
|
|
214
|
+
change_dir,
|
|
215
|
+
journal,
|
|
216
|
+
node_id=node_id,
|
|
217
|
+
ok=False,
|
|
218
|
+
code=code,
|
|
219
|
+
subject=normalized_subject,
|
|
220
|
+
)
|
|
221
|
+
return {
|
|
222
|
+
"ok": False,
|
|
223
|
+
"code": code,
|
|
224
|
+
"message": f"required predecessor is not complete: {predecessor_id}",
|
|
225
|
+
"predecessor": predecessor_id,
|
|
226
|
+
}
|
|
227
|
+
if predecessor.get("subject") != normalized_subject:
|
|
228
|
+
code = "SEQUENCE_SUBJECT_CHANGED"
|
|
229
|
+
_record_sequence_attempt(
|
|
230
|
+
change_dir,
|
|
231
|
+
journal,
|
|
232
|
+
node_id=node_id,
|
|
233
|
+
ok=False,
|
|
234
|
+
code=code,
|
|
235
|
+
subject=normalized_subject,
|
|
236
|
+
)
|
|
237
|
+
return {
|
|
238
|
+
"ok": False,
|
|
239
|
+
"code": code,
|
|
240
|
+
"message": "candidate subject differs from predecessor receipt",
|
|
241
|
+
"expected": predecessor.get("subject"),
|
|
242
|
+
"actual": normalized_subject,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
freeze_node_id = str(contract.get("freezeNodeId") or "")
|
|
246
|
+
freeze_receipt = receipts.get(freeze_node_id)
|
|
247
|
+
if (
|
|
248
|
+
freeze_node_id
|
|
249
|
+
and isinstance(freeze_receipt, dict)
|
|
250
|
+
and freeze_receipt.get("subject") != normalized_subject
|
|
251
|
+
):
|
|
252
|
+
code = "SEQUENCE_SUBJECT_CHANGED"
|
|
253
|
+
_record_sequence_attempt(
|
|
254
|
+
change_dir,
|
|
255
|
+
journal,
|
|
256
|
+
node_id=node_id,
|
|
257
|
+
ok=False,
|
|
258
|
+
code=code,
|
|
259
|
+
subject=normalized_subject,
|
|
260
|
+
)
|
|
261
|
+
return {
|
|
262
|
+
"ok": False,
|
|
263
|
+
"code": code,
|
|
264
|
+
"message": "candidate subject changed after code-freeze",
|
|
265
|
+
"expected": freeze_receipt.get("subject"),
|
|
266
|
+
"actual": normalized_subject,
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
normalized_status = status.strip().upper()
|
|
270
|
+
if normalized_status not in {"OK", "WARN", "FAIL"}:
|
|
271
|
+
return {
|
|
272
|
+
"ok": False,
|
|
273
|
+
"code": "SEQUENCE_STATUS_INVALID",
|
|
274
|
+
"message": f"unsupported final-sequence status: {status}",
|
|
275
|
+
}
|
|
276
|
+
normalized_evidence = evidence if isinstance(evidence, dict) else {}
|
|
277
|
+
if node_id == "sequence:unit-test-full":
|
|
278
|
+
full_execution_id = str(
|
|
279
|
+
normalized_evidence.get("fullExecutionId") or ""
|
|
280
|
+
).strip()
|
|
281
|
+
try:
|
|
282
|
+
environment_journal = load_environment_execution_journal(
|
|
283
|
+
change_dir
|
|
284
|
+
)
|
|
285
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
286
|
+
environment_journal = {"fullExecutions": []}
|
|
287
|
+
environment_error = str(exc)
|
|
288
|
+
else:
|
|
289
|
+
environment_error = ""
|
|
290
|
+
full_execution = next(
|
|
291
|
+
(
|
|
292
|
+
item
|
|
293
|
+
for item in environment_journal.get("fullExecutions") or []
|
|
294
|
+
if isinstance(item, dict)
|
|
295
|
+
and item.get("fullExecutionId") == full_execution_id
|
|
296
|
+
),
|
|
297
|
+
None,
|
|
298
|
+
)
|
|
299
|
+
if (
|
|
300
|
+
not full_execution_id
|
|
301
|
+
or not isinstance(full_execution, dict)
|
|
302
|
+
or full_execution.get("subject") != normalized_subject
|
|
303
|
+
or full_execution.get("status") != "OK"
|
|
304
|
+
or full_execution.get("assertionBearing") is not True
|
|
305
|
+
):
|
|
306
|
+
code = "FULL_EXECUTION_EVIDENCE_REQUIRED"
|
|
307
|
+
_record_sequence_attempt(
|
|
308
|
+
change_dir,
|
|
309
|
+
journal,
|
|
310
|
+
node_id=node_id,
|
|
311
|
+
ok=False,
|
|
312
|
+
code=code,
|
|
313
|
+
subject=normalized_subject,
|
|
314
|
+
)
|
|
315
|
+
return {
|
|
316
|
+
"ok": False,
|
|
317
|
+
"code": code,
|
|
318
|
+
"message": (
|
|
319
|
+
"unit-test-full requires a successful assertion-bearing "
|
|
320
|
+
"Full execution bound to the frozen subject"
|
|
321
|
+
),
|
|
322
|
+
"fullExecutionId": full_execution_id or None,
|
|
323
|
+
"journalError": environment_error or None,
|
|
324
|
+
}
|
|
325
|
+
receipt = {
|
|
326
|
+
"schemaVersion": 1,
|
|
327
|
+
"nodeId": node_id,
|
|
328
|
+
"status": normalized_status,
|
|
329
|
+
"subject": normalized_subject,
|
|
330
|
+
"evidence": normalized_evidence,
|
|
331
|
+
"recordedAt": he.now_iso(),
|
|
332
|
+
}
|
|
333
|
+
existing = receipts.get(node_id)
|
|
334
|
+
reused = (
|
|
335
|
+
isinstance(existing, dict)
|
|
336
|
+
and existing.get("status") == normalized_status
|
|
337
|
+
and existing.get("subject") == normalized_subject
|
|
338
|
+
and existing.get("evidence") == receipt["evidence"]
|
|
339
|
+
)
|
|
340
|
+
if not reused:
|
|
341
|
+
receipts[node_id] = receipt
|
|
342
|
+
_record_sequence_attempt(
|
|
343
|
+
change_dir,
|
|
344
|
+
journal,
|
|
345
|
+
node_id=node_id,
|
|
346
|
+
ok=normalized_status == "OK",
|
|
347
|
+
code=(
|
|
348
|
+
"SEQUENCE_RECEIPT_REUSED"
|
|
349
|
+
if reused
|
|
350
|
+
else (
|
|
351
|
+
"SEQUENCE_STEP_RECORDED"
|
|
352
|
+
if normalized_status == "OK"
|
|
353
|
+
else "SEQUENCE_STEP_FAILED"
|
|
354
|
+
)
|
|
355
|
+
),
|
|
356
|
+
subject=normalized_subject,
|
|
357
|
+
)
|
|
358
|
+
ok = normalized_status == "OK"
|
|
359
|
+
return {
|
|
360
|
+
"ok": ok,
|
|
361
|
+
"code": (
|
|
362
|
+
"SEQUENCE_RECEIPT_REUSED"
|
|
363
|
+
if reused
|
|
364
|
+
else ("SEQUENCE_STEP_RECORDED" if ok else "SEQUENCE_STEP_FAILED")
|
|
365
|
+
),
|
|
366
|
+
"nodeId": node_id,
|
|
367
|
+
"receipt": receipts.get(node_id),
|
|
368
|
+
"reused": reused,
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def evaluate_final_sequence(
|
|
373
|
+
change_dir: Path,
|
|
374
|
+
subject: dict[str, Any],
|
|
375
|
+
*,
|
|
376
|
+
exclude_nodes: set[str] | None = None,
|
|
377
|
+
) -> dict[str, Any]:
|
|
378
|
+
"""Evaluate all required sequence receipts against one candidate subject."""
|
|
379
|
+
change_dir = change_dir.resolve()
|
|
380
|
+
try:
|
|
381
|
+
contract = _final_sequence_contract(change_dir)
|
|
382
|
+
if contract is None:
|
|
383
|
+
return {
|
|
384
|
+
"ok": True,
|
|
385
|
+
"code": "FINAL_SEQUENCE_NOT_CONFIGURED",
|
|
386
|
+
"skipped": True,
|
|
387
|
+
"missing": [],
|
|
388
|
+
"invalid": [],
|
|
389
|
+
}
|
|
390
|
+
normalized_subject = _validated_sequence_subject(subject)
|
|
391
|
+
journal = _load_sequence_receipts(change_dir)
|
|
392
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
393
|
+
return {
|
|
394
|
+
"ok": False,
|
|
395
|
+
"code": "FINAL_SEQUENCE_INVALID",
|
|
396
|
+
"message": str(exc),
|
|
397
|
+
}
|
|
398
|
+
excluded = exclude_nodes or set()
|
|
399
|
+
required = [
|
|
400
|
+
str(item)
|
|
401
|
+
for item in contract.get("nodeIds") or []
|
|
402
|
+
if isinstance(item, str) and item not in excluded
|
|
403
|
+
]
|
|
404
|
+
receipts = journal["receipts"]
|
|
405
|
+
missing = [node_id for node_id in required if node_id not in receipts]
|
|
406
|
+
invalid = []
|
|
407
|
+
for node_id in required:
|
|
408
|
+
receipt = receipts.get(node_id)
|
|
409
|
+
if not isinstance(receipt, dict):
|
|
410
|
+
continue
|
|
411
|
+
if (
|
|
412
|
+
str(receipt.get("status") or "").upper() != "OK"
|
|
413
|
+
or receipt.get("subject") != normalized_subject
|
|
414
|
+
):
|
|
415
|
+
invalid.append(
|
|
416
|
+
{
|
|
417
|
+
"nodeId": node_id,
|
|
418
|
+
"status": receipt.get("status"),
|
|
419
|
+
"subject": receipt.get("subject"),
|
|
420
|
+
}
|
|
421
|
+
)
|
|
422
|
+
ok = not missing and not invalid
|
|
423
|
+
return {
|
|
424
|
+
"ok": ok,
|
|
425
|
+
"code": (
|
|
426
|
+
"FINAL_SEQUENCE_COMPLETE"
|
|
427
|
+
if ok
|
|
428
|
+
else "FINAL_SEQUENCE_INCOMPLETE"
|
|
429
|
+
),
|
|
430
|
+
"message": (
|
|
431
|
+
"all required final-sequence receipts match the candidate"
|
|
432
|
+
if ok
|
|
433
|
+
else f"missing={len(missing)} invalid={len(invalid)}"
|
|
434
|
+
),
|
|
435
|
+
"subject": normalized_subject,
|
|
436
|
+
"required": required,
|
|
437
|
+
"missing": missing,
|
|
438
|
+
"invalid": invalid,
|
|
439
|
+
"receiptsPath": str(change_dir / FINAL_SEQUENCE_RECEIPTS_REL),
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def load_environment_execution_journal(
|
|
444
|
+
change_dir: Path,
|
|
445
|
+
) -> dict[str, Any]:
|
|
446
|
+
path = change_dir.resolve() / ENVIRONMENT_EXECUTIONS_REL
|
|
447
|
+
if not path.is_file():
|
|
448
|
+
return {
|
|
449
|
+
"schemaVersion": 1,
|
|
450
|
+
"changeId": change_dir.name,
|
|
451
|
+
"environmentAttempts": [],
|
|
452
|
+
"fullExecutions": [],
|
|
453
|
+
}
|
|
454
|
+
journal = _read_json(path)
|
|
455
|
+
if journal.get("schemaVersion") != 1:
|
|
456
|
+
raise ValueError("unsupported environment execution journal schema")
|
|
457
|
+
for field in ("environmentAttempts", "fullExecutions"):
|
|
458
|
+
if not isinstance(journal.get(field), list):
|
|
459
|
+
raise ValueError(f"environment journal {field} must be an array")
|
|
460
|
+
return journal
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def record_environment_attempt(
|
|
464
|
+
change_dir: Path,
|
|
465
|
+
subject: dict[str, Any],
|
|
466
|
+
*,
|
|
467
|
+
stage: str,
|
|
468
|
+
status: str,
|
|
469
|
+
duration_ms: int,
|
|
470
|
+
failure_signature: str | None = None,
|
|
471
|
+
) -> dict[str, Any]:
|
|
472
|
+
"""Record environment lifecycle work without manufacturing Full results."""
|
|
473
|
+
change_dir = change_dir.resolve()
|
|
474
|
+
if stage not in {"prepare", "verify", "release"}:
|
|
475
|
+
return {
|
|
476
|
+
"ok": False,
|
|
477
|
+
"code": "ENVIRONMENT_STAGE_INVALID",
|
|
478
|
+
"message": f"unsupported environment stage: {stage}",
|
|
479
|
+
}
|
|
480
|
+
normalized_status = status.strip().upper()
|
|
481
|
+
if normalized_status not in {
|
|
482
|
+
"OK",
|
|
483
|
+
"ENVIRONMENT_ERROR",
|
|
484
|
+
"INTERRUPTED",
|
|
485
|
+
"FAIL",
|
|
486
|
+
}:
|
|
487
|
+
return {
|
|
488
|
+
"ok": False,
|
|
489
|
+
"code": "ENVIRONMENT_STATUS_INVALID",
|
|
490
|
+
"message": f"unsupported environment status: {status}",
|
|
491
|
+
}
|
|
492
|
+
if not isinstance(duration_ms, int) or duration_ms < 0:
|
|
493
|
+
return {
|
|
494
|
+
"ok": False,
|
|
495
|
+
"code": "ENVIRONMENT_DURATION_INVALID",
|
|
496
|
+
"message": "duration_ms must be a non-negative integer",
|
|
497
|
+
}
|
|
498
|
+
try:
|
|
499
|
+
normalized_subject = _validated_sequence_subject(subject)
|
|
500
|
+
journal = load_environment_execution_journal(change_dir)
|
|
501
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
502
|
+
return {
|
|
503
|
+
"ok": False,
|
|
504
|
+
"code": "ENVIRONMENT_JOURNAL_INVALID",
|
|
505
|
+
"message": str(exc),
|
|
506
|
+
}
|
|
507
|
+
attempt = {
|
|
508
|
+
"attemptId": "env-" + uuid.uuid4().hex,
|
|
509
|
+
"stage": stage,
|
|
510
|
+
"status": normalized_status,
|
|
511
|
+
"subject": normalized_subject,
|
|
512
|
+
"durationMs": duration_ms,
|
|
513
|
+
"failureSignature": failure_signature,
|
|
514
|
+
"recordedAt": he.now_iso(),
|
|
515
|
+
}
|
|
516
|
+
journal["environmentAttempts"].append(attempt)
|
|
517
|
+
_write_json(change_dir / ENVIRONMENT_EXECUTIONS_REL, journal)
|
|
518
|
+
ok = normalized_status == "OK"
|
|
519
|
+
return {
|
|
520
|
+
"ok": ok,
|
|
521
|
+
"code": (
|
|
522
|
+
"ENVIRONMENT_STAGE_RECORDED"
|
|
523
|
+
if ok
|
|
524
|
+
else "ENVIRONMENT_ERROR_RECORDED"
|
|
525
|
+
),
|
|
526
|
+
"attempt": attempt,
|
|
527
|
+
"fullExecutionCreated": False,
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def record_full_test_execution(
|
|
532
|
+
change_dir: Path,
|
|
533
|
+
subject: dict[str, Any],
|
|
534
|
+
*,
|
|
535
|
+
status: str,
|
|
536
|
+
prepare_ms: int,
|
|
537
|
+
test_ms: int,
|
|
538
|
+
cleanup_ms: int,
|
|
539
|
+
result_digest: str,
|
|
540
|
+
supersedes: str | None = None,
|
|
541
|
+
) -> dict[str, Any]:
|
|
542
|
+
"""Record one assertion-bearing Full execution after environment success."""
|
|
543
|
+
change_dir = change_dir.resolve()
|
|
544
|
+
normalized_status = status.strip().upper()
|
|
545
|
+
if normalized_status not in {"OK", "FAIL"}:
|
|
546
|
+
return {
|
|
547
|
+
"ok": False,
|
|
548
|
+
"code": "FULL_STATUS_INVALID",
|
|
549
|
+
"message": "Full status must be OK or FAIL",
|
|
550
|
+
}
|
|
551
|
+
durations = (prepare_ms, test_ms, cleanup_ms)
|
|
552
|
+
if any(not isinstance(value, int) or value < 0 for value in durations):
|
|
553
|
+
return {
|
|
554
|
+
"ok": False,
|
|
555
|
+
"code": "FULL_DURATION_INVALID",
|
|
556
|
+
"message": "prepare/test/cleanup durations must be non-negative",
|
|
557
|
+
}
|
|
558
|
+
if not re.fullmatch(r"sha256:[0-9a-fA-F]{64}", result_digest):
|
|
559
|
+
return {
|
|
560
|
+
"ok": False,
|
|
561
|
+
"code": "FULL_RESULT_DIGEST_INVALID",
|
|
562
|
+
"message": "result_digest must be a sha256 digest",
|
|
563
|
+
}
|
|
564
|
+
try:
|
|
565
|
+
normalized_subject = _validated_sequence_subject(subject)
|
|
566
|
+
journal = load_environment_execution_journal(change_dir)
|
|
567
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
568
|
+
return {
|
|
569
|
+
"ok": False,
|
|
570
|
+
"code": "ENVIRONMENT_JOURNAL_INVALID",
|
|
571
|
+
"message": str(exc),
|
|
572
|
+
}
|
|
573
|
+
successful_environment = {
|
|
574
|
+
str(item.get("stage") or ""): item
|
|
575
|
+
for item in journal["environmentAttempts"]
|
|
576
|
+
if isinstance(item, dict)
|
|
577
|
+
and item.get("subject") == normalized_subject
|
|
578
|
+
and str(item.get("status") or "").upper() == "OK"
|
|
579
|
+
}
|
|
580
|
+
missing = [
|
|
581
|
+
stage
|
|
582
|
+
for stage in ("prepare", "verify")
|
|
583
|
+
if stage not in successful_environment
|
|
584
|
+
]
|
|
585
|
+
if missing:
|
|
586
|
+
return {
|
|
587
|
+
"ok": False,
|
|
588
|
+
"code": "ENVIRONMENT_NOT_READY",
|
|
589
|
+
"message": (
|
|
590
|
+
"Full execution requires successful environment stages: "
|
|
591
|
+
+ ", ".join(missing)
|
|
592
|
+
),
|
|
593
|
+
"missing": missing,
|
|
594
|
+
}
|
|
595
|
+
prior_success = next(
|
|
596
|
+
(
|
|
597
|
+
item
|
|
598
|
+
for item in reversed(journal["fullExecutions"])
|
|
599
|
+
if isinstance(item, dict)
|
|
600
|
+
and item.get("subject") == normalized_subject
|
|
601
|
+
and item.get("status") == "OK"
|
|
602
|
+
),
|
|
603
|
+
None,
|
|
604
|
+
)
|
|
605
|
+
if prior_success is not None:
|
|
606
|
+
if prior_success.get("resultDigest") == result_digest:
|
|
607
|
+
sequence_receipt = record_sequence_receipt(
|
|
608
|
+
change_dir,
|
|
609
|
+
"sequence:unit-test-full",
|
|
610
|
+
normalized_subject,
|
|
611
|
+
evidence={
|
|
612
|
+
"fullExecutionId": prior_success.get("fullExecutionId"),
|
|
613
|
+
"resultDigest": result_digest,
|
|
614
|
+
"assertionBearing": True,
|
|
615
|
+
},
|
|
616
|
+
)
|
|
617
|
+
return {
|
|
618
|
+
"ok": True,
|
|
619
|
+
"code": "FULL_EXECUTION_REUSED",
|
|
620
|
+
"execution": prior_success,
|
|
621
|
+
"reused": True,
|
|
622
|
+
"sequenceReceipt": sequence_receipt,
|
|
623
|
+
}
|
|
624
|
+
return {
|
|
625
|
+
"ok": False,
|
|
626
|
+
"code": "FULL_ALREADY_COMPLETED",
|
|
627
|
+
"message": (
|
|
628
|
+
"a successful assertion-bearing Full already exists for this identity"
|
|
629
|
+
),
|
|
630
|
+
"existingExecutionId": prior_success.get("fullExecutionId"),
|
|
631
|
+
}
|
|
632
|
+
prior_failed_ids = {
|
|
633
|
+
str(item.get("fullExecutionId") or "")
|
|
634
|
+
for item in journal["fullExecutions"]
|
|
635
|
+
if isinstance(item, dict)
|
|
636
|
+
and item.get("subject") == normalized_subject
|
|
637
|
+
and item.get("status") == "FAIL"
|
|
638
|
+
}
|
|
639
|
+
if prior_failed_ids and supersedes not in prior_failed_ids:
|
|
640
|
+
return {
|
|
641
|
+
"ok": False,
|
|
642
|
+
"code": "FULL_SUPERSESSION_REQUIRED",
|
|
643
|
+
"message": "Full rerun after assertion failure must name supersedes",
|
|
644
|
+
"priorFailedExecutionIds": sorted(prior_failed_ids),
|
|
645
|
+
}
|
|
646
|
+
execution = {
|
|
647
|
+
"fullExecutionId": "full-" + uuid.uuid4().hex,
|
|
648
|
+
"status": normalized_status,
|
|
649
|
+
"subject": normalized_subject,
|
|
650
|
+
"environmentAttemptIds": [
|
|
651
|
+
successful_environment[stage]["attemptId"]
|
|
652
|
+
for stage in ("prepare", "verify")
|
|
653
|
+
],
|
|
654
|
+
"prepareMs": prepare_ms,
|
|
655
|
+
"testExecutionMs": test_ms,
|
|
656
|
+
"cleanupMs": cleanup_ms,
|
|
657
|
+
"totalMs": prepare_ms + test_ms + cleanup_ms,
|
|
658
|
+
"resultDigest": result_digest,
|
|
659
|
+
"supersedes": supersedes,
|
|
660
|
+
"assertionBearing": True,
|
|
661
|
+
"recordedAt": he.now_iso(),
|
|
662
|
+
}
|
|
663
|
+
journal["fullExecutions"].append(execution)
|
|
664
|
+
_write_json(change_dir / ENVIRONMENT_EXECUTIONS_REL, journal)
|
|
665
|
+
sequence_receipt = None
|
|
666
|
+
if normalized_status == "OK":
|
|
667
|
+
sequence_receipt = record_sequence_receipt(
|
|
668
|
+
change_dir,
|
|
669
|
+
"sequence:unit-test-full",
|
|
670
|
+
normalized_subject,
|
|
671
|
+
evidence={
|
|
672
|
+
"fullExecutionId": execution["fullExecutionId"],
|
|
673
|
+
"resultDigest": result_digest,
|
|
674
|
+
"assertionBearing": True,
|
|
675
|
+
},
|
|
676
|
+
)
|
|
677
|
+
return {
|
|
678
|
+
"ok": normalized_status == "OK",
|
|
679
|
+
"code": (
|
|
680
|
+
"FULL_EXECUTION_RECORDED"
|
|
681
|
+
if normalized_status == "OK"
|
|
682
|
+
else "FULL_ASSERTIONS_FAILED"
|
|
683
|
+
),
|
|
684
|
+
"execution": execution,
|
|
685
|
+
"reused": False,
|
|
686
|
+
"sequenceReceipt": sequence_receipt,
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
|
|
69
690
|
def _stable_hex(*parts: Any, length: int) -> str:
|
|
70
691
|
raw = "\x1f".join(str(part) for part in parts).encode("utf-8")
|
|
71
692
|
return hashlib.sha256(raw).hexdigest()[:length]
|
|
@@ -667,9 +1288,26 @@ def collect_node_evidence(
|
|
|
667
1288
|
) -> dict[str, dict[str, Any]]:
|
|
668
1289
|
result: dict[str, dict[str, Any]] = {}
|
|
669
1290
|
validations = (ledger or {}).get("validations") or {}
|
|
1291
|
+
sequence_receipts = _load_sequence_receipts(change_dir).get("receipts") or {}
|
|
670
1292
|
for node in dag.get("nodes") or []:
|
|
671
1293
|
node_id = str(node.get("id") or "")
|
|
672
|
-
if node.get("kind") == "
|
|
1294
|
+
if node.get("kind") == "sequence" or node_id.startswith("sequence:"):
|
|
1295
|
+
receipt = (
|
|
1296
|
+
sequence_receipts.get(node_id)
|
|
1297
|
+
if isinstance(sequence_receipts, dict)
|
|
1298
|
+
else None
|
|
1299
|
+
)
|
|
1300
|
+
status = (
|
|
1301
|
+
str(receipt.get("status") or "").upper()
|
|
1302
|
+
if isinstance(receipt, dict)
|
|
1303
|
+
else ""
|
|
1304
|
+
)
|
|
1305
|
+
result[node_id] = {
|
|
1306
|
+
"reusable": status == "OK",
|
|
1307
|
+
"reason": f"sequence-status:{status or 'missing'}",
|
|
1308
|
+
"receipt": receipt,
|
|
1309
|
+
}
|
|
1310
|
+
elif node.get("kind") == "validation" or node_id.startswith("validation:"):
|
|
673
1311
|
verification = node_id.split(":", 1)[1]
|
|
674
1312
|
entry = validations.get(verification) if isinstance(validations, dict) else None
|
|
675
1313
|
if not isinstance(entry, dict):
|
|
@@ -895,6 +1533,78 @@ def _cmd_metrics(args: argparse.Namespace) -> int:
|
|
|
895
1533
|
return 0
|
|
896
1534
|
|
|
897
1535
|
|
|
1536
|
+
def _cmd_record_sequence(args: argparse.Namespace) -> int:
|
|
1537
|
+
result = record_sequence_receipt(
|
|
1538
|
+
Path(args.change_dir),
|
|
1539
|
+
args.node,
|
|
1540
|
+
{
|
|
1541
|
+
"productCommit": args.product_commit,
|
|
1542
|
+
"productTreeHash": args.product_tree_hash,
|
|
1543
|
+
"environmentHash": args.environment_hash,
|
|
1544
|
+
},
|
|
1545
|
+
status=args.status,
|
|
1546
|
+
)
|
|
1547
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
1548
|
+
return 0 if result.get("ok") else 2
|
|
1549
|
+
|
|
1550
|
+
|
|
1551
|
+
def _cmd_sequence_status(args: argparse.Namespace) -> int:
|
|
1552
|
+
result = evaluate_final_sequence(
|
|
1553
|
+
Path(args.change_dir),
|
|
1554
|
+
{
|
|
1555
|
+
"productCommit": args.product_commit,
|
|
1556
|
+
"productTreeHash": args.product_tree_hash,
|
|
1557
|
+
"environmentHash": args.environment_hash,
|
|
1558
|
+
},
|
|
1559
|
+
exclude_nodes={"sequence:archive"} if args.pre_archive else set(),
|
|
1560
|
+
)
|
|
1561
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
1562
|
+
return 0 if result.get("ok") else 2
|
|
1563
|
+
|
|
1564
|
+
|
|
1565
|
+
def _subject_from_args(args: argparse.Namespace) -> dict[str, str]:
|
|
1566
|
+
return {
|
|
1567
|
+
"productCommit": args.product_commit,
|
|
1568
|
+
"productTreeHash": args.product_tree_hash,
|
|
1569
|
+
"environmentHash": args.environment_hash,
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
|
|
1573
|
+
def _cmd_record_environment(args: argparse.Namespace) -> int:
|
|
1574
|
+
result = record_environment_attempt(
|
|
1575
|
+
Path(args.change_dir),
|
|
1576
|
+
_subject_from_args(args),
|
|
1577
|
+
stage=args.stage,
|
|
1578
|
+
status=args.status,
|
|
1579
|
+
duration_ms=args.duration_ms,
|
|
1580
|
+
failure_signature=args.failure_signature,
|
|
1581
|
+
)
|
|
1582
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
1583
|
+
return 0 if result.get("ok") else 2
|
|
1584
|
+
|
|
1585
|
+
|
|
1586
|
+
def _cmd_record_full(args: argparse.Namespace) -> int:
|
|
1587
|
+
result = record_full_test_execution(
|
|
1588
|
+
Path(args.change_dir),
|
|
1589
|
+
_subject_from_args(args),
|
|
1590
|
+
status=args.status,
|
|
1591
|
+
prepare_ms=args.prepare_ms,
|
|
1592
|
+
test_ms=args.test_ms,
|
|
1593
|
+
cleanup_ms=args.cleanup_ms,
|
|
1594
|
+
result_digest=args.result_digest,
|
|
1595
|
+
supersedes=args.supersedes,
|
|
1596
|
+
)
|
|
1597
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
1598
|
+
return 0 if result.get("ok") else 2
|
|
1599
|
+
|
|
1600
|
+
|
|
1601
|
+
def _add_subject_arguments(parser: argparse.ArgumentParser) -> None:
|
|
1602
|
+
parser.add_argument("--change-dir", required=True)
|
|
1603
|
+
parser.add_argument("--product-commit", required=True)
|
|
1604
|
+
parser.add_argument("--product-tree-hash", required=True)
|
|
1605
|
+
parser.add_argument("--environment-hash", required=True)
|
|
1606
|
+
|
|
1607
|
+
|
|
898
1608
|
def build_parser() -> argparse.ArgumentParser:
|
|
899
1609
|
parser = argparse.ArgumentParser(prog="harness_phase.py")
|
|
900
1610
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
@@ -914,6 +1624,53 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
914
1624
|
metrics_parser.add_argument("--head-sha")
|
|
915
1625
|
metrics_parser.add_argument("--output")
|
|
916
1626
|
metrics_parser.set_defaults(func=_cmd_metrics)
|
|
1627
|
+
|
|
1628
|
+
record_parser = sub.add_parser("record-sequence")
|
|
1629
|
+
record_parser.add_argument("--change-dir", required=True)
|
|
1630
|
+
record_parser.add_argument("--node", required=True)
|
|
1631
|
+
record_parser.add_argument("--product-commit", required=True)
|
|
1632
|
+
record_parser.add_argument("--product-tree-hash", required=True)
|
|
1633
|
+
record_parser.add_argument("--environment-hash", required=True)
|
|
1634
|
+
record_parser.add_argument(
|
|
1635
|
+
"--status",
|
|
1636
|
+
choices=("OK", "WARN", "FAIL"),
|
|
1637
|
+
default="OK",
|
|
1638
|
+
)
|
|
1639
|
+
record_parser.set_defaults(func=_cmd_record_sequence)
|
|
1640
|
+
|
|
1641
|
+
sequence_parser = sub.add_parser("sequence-status")
|
|
1642
|
+
sequence_parser.add_argument("--change-dir", required=True)
|
|
1643
|
+
sequence_parser.add_argument("--product-commit", required=True)
|
|
1644
|
+
sequence_parser.add_argument("--product-tree-hash", required=True)
|
|
1645
|
+
sequence_parser.add_argument("--environment-hash", required=True)
|
|
1646
|
+
sequence_parser.add_argument("--pre-archive", action="store_true")
|
|
1647
|
+
sequence_parser.set_defaults(func=_cmd_sequence_status)
|
|
1648
|
+
|
|
1649
|
+
environment_parser = sub.add_parser("record-environment")
|
|
1650
|
+
_add_subject_arguments(environment_parser)
|
|
1651
|
+
environment_parser.add_argument(
|
|
1652
|
+
"--stage",
|
|
1653
|
+
choices=("prepare", "verify", "release"),
|
|
1654
|
+
required=True,
|
|
1655
|
+
)
|
|
1656
|
+
environment_parser.add_argument(
|
|
1657
|
+
"--status",
|
|
1658
|
+
choices=("OK", "ENVIRONMENT_ERROR", "INTERRUPTED", "FAIL"),
|
|
1659
|
+
required=True,
|
|
1660
|
+
)
|
|
1661
|
+
environment_parser.add_argument("--duration-ms", type=int, required=True)
|
|
1662
|
+
environment_parser.add_argument("--failure-signature")
|
|
1663
|
+
environment_parser.set_defaults(func=_cmd_record_environment)
|
|
1664
|
+
|
|
1665
|
+
full_parser = sub.add_parser("record-full")
|
|
1666
|
+
_add_subject_arguments(full_parser)
|
|
1667
|
+
full_parser.add_argument("--status", choices=("OK", "FAIL"), required=True)
|
|
1668
|
+
full_parser.add_argument("--prepare-ms", type=int, required=True)
|
|
1669
|
+
full_parser.add_argument("--test-ms", type=int, required=True)
|
|
1670
|
+
full_parser.add_argument("--cleanup-ms", type=int, required=True)
|
|
1671
|
+
full_parser.add_argument("--result-digest", required=True)
|
|
1672
|
+
full_parser.add_argument("--supersedes")
|
|
1673
|
+
full_parser.set_defaults(func=_cmd_record_full)
|
|
917
1674
|
return parser
|
|
918
1675
|
|
|
919
1676
|
|