@hunter-harness/workflow-harness 0.2.27 → 0.2.29
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/contracts/workflow-policy.json +34 -2
- 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 +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +26 -2
- package/harness/bundles/general/claude-code/harness-test/checklist.md +15 -0
- package/harness/bundles/general/claude-code/harness-test/reference.md +28 -4
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +2061 -187
- package/harness/bundles/general/claude-code/scripts/harness_events.py +43 -9
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +491 -7
- package/harness/bundles/general/claude-code/scripts/harness_integration.py +232 -0
- 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/claude-code/scripts/harness_test_runner.py +974 -0
- package/harness/bundles/general/claude-code/scripts/harness_workflow_policy.py +87 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/contracts/workflow-policy.json +34 -2
- 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 +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +26 -2
- package/harness/bundles/general/codebuddy/harness-test/checklist.md +15 -0
- package/harness/bundles/general/codebuddy/harness-test/reference.md +28 -4
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +2061 -187
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +43 -9
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +491 -7
- package/harness/bundles/general/codebuddy/scripts/harness_integration.py +232 -0
- 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/codebuddy/scripts/harness_test_runner.py +974 -0
- package/harness/bundles/general/codebuddy/scripts/harness_workflow_policy.py +87 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/contracts/workflow-policy.json +34 -2
- 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 +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +26 -2
- package/harness/bundles/general/codex/harness-test/checklist.md +15 -0
- package/harness/bundles/general/codex/harness-test/reference.md +28 -4
- package/harness/bundles/general/codex/scripts/harness_archive.py +2061 -187
- package/harness/bundles/general/codex/scripts/harness_events.py +43 -9
- package/harness/bundles/general/codex/scripts/harness_gate.py +491 -7
- package/harness/bundles/general/codex/scripts/harness_integration.py +232 -0
- 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/codex/scripts/harness_test_runner.py +974 -0
- package/harness/bundles/general/codex/scripts/harness_workflow_policy.py +87 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/contracts/workflow-policy.json +34 -2
- 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 +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +26 -2
- package/harness/bundles/general/cursor/harness-test/checklist.md +15 -0
- package/harness/bundles/general/cursor/harness-test/reference.md +28 -4
- package/harness/bundles/general/cursor/scripts/harness_archive.py +2061 -187
- package/harness/bundles/general/cursor/scripts/harness_events.py +43 -9
- package/harness/bundles/general/cursor/scripts/harness_gate.py +491 -7
- package/harness/bundles/general/cursor/scripts/harness_integration.py +232 -0
- 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/general/cursor/scripts/harness_test_runner.py +974 -0
- package/harness/bundles/general/cursor/scripts/harness_workflow_policy.py +87 -0
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/contracts/workflow-policy.json +34 -2
- 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 +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +26 -2
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +2061 -187
- package/harness/bundles/java/claude-code/scripts/harness_events.py +43 -9
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +491 -7
- package/harness/bundles/java/claude-code/scripts/harness_integration.py +232 -0
- 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/claude-code/scripts/harness_test_runner.py +974 -0
- package/harness/bundles/java/claude-code/scripts/harness_workflow_policy.py +87 -0
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/contracts/workflow-policy.json +34 -2
- 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 +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +26 -2
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +2061 -187
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +43 -9
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +491 -7
- package/harness/bundles/java/codebuddy/scripts/harness_integration.py +232 -0
- 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/codebuddy/scripts/harness_test_runner.py +974 -0
- package/harness/bundles/java/codebuddy/scripts/harness_workflow_policy.py +87 -0
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/contracts/workflow-policy.json +34 -2
- 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 +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +26 -2
- package/harness/bundles/java/codex/scripts/harness_archive.py +2061 -187
- package/harness/bundles/java/codex/scripts/harness_events.py +43 -9
- package/harness/bundles/java/codex/scripts/harness_gate.py +491 -7
- package/harness/bundles/java/codex/scripts/harness_integration.py +232 -0
- 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/codex/scripts/harness_test_runner.py +974 -0
- package/harness/bundles/java/codex/scripts/harness_workflow_policy.py +87 -0
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/contracts/workflow-policy.json +34 -2
- 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 +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +26 -2
- package/harness/bundles/java/cursor/scripts/harness_archive.py +2061 -187
- package/harness/bundles/java/cursor/scripts/harness_events.py +43 -9
- package/harness/bundles/java/cursor/scripts/harness_gate.py +491 -7
- package/harness/bundles/java/cursor/scripts/harness_integration.py +232 -0
- 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/bundles/java/cursor/scripts/harness_test_runner.py +974 -0
- package/harness/bundles/java/cursor/scripts/harness_workflow_policy.py +87 -0
- package/harness/manifests/general/claude-code.json +34 -26
- package/harness/manifests/general/codebuddy.json +34 -26
- package/harness/manifests/general/codex.json +34 -26
- package/harness/manifests/general/cursor.json +34 -26
- package/harness/manifests/java/claude-code.json +34 -26
- package/harness/manifests/java/codebuddy.json +34 -26
- package/harness/manifests/java/codex.json +34 -26
- package/harness/manifests/java/cursor.json +34 -26
- package/hunter-workflow-family.json +2 -2
- package/package.json +1 -1
|
@@ -160,6 +160,118 @@ def _git_is_ancestor(cwd: Path, ancestor: str, descendant: str) -> bool:
|
|
|
160
160
|
return proc.returncode == 0
|
|
161
161
|
|
|
162
162
|
|
|
163
|
+
def evaluate_projection_gate(
|
|
164
|
+
project: Path,
|
|
165
|
+
phase: str,
|
|
166
|
+
) -> dict[str, Any]:
|
|
167
|
+
"""Validate three-stage projection identity and apply phase fallback policy."""
|
|
168
|
+
project = project.resolve()
|
|
169
|
+
path = project / ".harness" / "config" / "projection-receipt.json"
|
|
170
|
+
release_phase = phase in {"submit", "archive", "release", "deploy"}
|
|
171
|
+
if not path.is_file():
|
|
172
|
+
return {
|
|
173
|
+
"ok": True,
|
|
174
|
+
"code": "PROJECTION_RECEIPT_NOT_CONFIGURED",
|
|
175
|
+
"status": "legacy",
|
|
176
|
+
"mode": "legacy",
|
|
177
|
+
"receiptPath": str(path),
|
|
178
|
+
"releasePhase": release_phase,
|
|
179
|
+
}
|
|
180
|
+
try:
|
|
181
|
+
receipt = _read_json(path)
|
|
182
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
183
|
+
receipt = {}
|
|
184
|
+
parse_error = str(exc)
|
|
185
|
+
else:
|
|
186
|
+
parse_error = None
|
|
187
|
+
source = receipt.get("source")
|
|
188
|
+
transform = receipt.get("transform")
|
|
189
|
+
target = receipt.get("target")
|
|
190
|
+
source = source if isinstance(source, dict) else {}
|
|
191
|
+
transform = transform if isinstance(transform, dict) else {}
|
|
192
|
+
target = target if isinstance(target, dict) else {}
|
|
193
|
+
digest_pattern = re.compile(r"sha256:[0-9a-fA-F]{64}")
|
|
194
|
+
issues = []
|
|
195
|
+
if receipt.get("schemaVersion") != 1:
|
|
196
|
+
issues.append("schemaVersion")
|
|
197
|
+
for label, value in (
|
|
198
|
+
("source.hash", source.get("hash")),
|
|
199
|
+
("transform.hash", transform.get("hash")),
|
|
200
|
+
("target.hash", target.get("hash")),
|
|
201
|
+
):
|
|
202
|
+
if not isinstance(value, str) or not digest_pattern.fullmatch(value):
|
|
203
|
+
issues.append(label)
|
|
204
|
+
if not str(transform.get("version") or "").strip():
|
|
205
|
+
issues.append("transform.version")
|
|
206
|
+
if not isinstance(receipt.get("projectOwnedExclusions"), list):
|
|
207
|
+
issues.append("projectOwnedExclusions")
|
|
208
|
+
if not str(receipt.get("generatedAt") or "").strip():
|
|
209
|
+
issues.append("generatedAt")
|
|
210
|
+
status = str(receipt.get("status") or "").strip().lower()
|
|
211
|
+
if status not in {"verified", "degraded"}:
|
|
212
|
+
issues.append("status")
|
|
213
|
+
status = "degraded"
|
|
214
|
+
if parse_error:
|
|
215
|
+
issues.append("parse")
|
|
216
|
+
receipt_hash = "sha256:" + hashlib.sha256(
|
|
217
|
+
json.dumps(
|
|
218
|
+
receipt,
|
|
219
|
+
ensure_ascii=False,
|
|
220
|
+
sort_keys=True,
|
|
221
|
+
separators=(",", ":"),
|
|
222
|
+
).encode("utf-8")
|
|
223
|
+
).hexdigest()
|
|
224
|
+
degraded = status == "degraded" or bool(issues)
|
|
225
|
+
if degraded and release_phase:
|
|
226
|
+
return {
|
|
227
|
+
"ok": False,
|
|
228
|
+
"code": "PROJECTION_DEGRADED_BLOCKED",
|
|
229
|
+
"message": (
|
|
230
|
+
"adapter/config projection is degraded; refresh the projection "
|
|
231
|
+
"receipt before submit/archive/release"
|
|
232
|
+
),
|
|
233
|
+
"status": "degraded",
|
|
234
|
+
"mode": "blocked",
|
|
235
|
+
"issues": issues,
|
|
236
|
+
"receipt": receipt,
|
|
237
|
+
"receiptHash": receipt_hash,
|
|
238
|
+
"receiptPath": str(path),
|
|
239
|
+
"releasePhase": True,
|
|
240
|
+
"remediation": (
|
|
241
|
+
"regenerate .harness/config/projection-receipt.json from the "
|
|
242
|
+
"project truth source"
|
|
243
|
+
),
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
"ok": True,
|
|
247
|
+
"code": (
|
|
248
|
+
"PROJECTION_VERIFIED"
|
|
249
|
+
if not degraded
|
|
250
|
+
else "PROJECTION_DEGRADED_FALLBACK"
|
|
251
|
+
),
|
|
252
|
+
"message": (
|
|
253
|
+
"projection receipt verified"
|
|
254
|
+
if not degraded
|
|
255
|
+
else "degraded projection allowed only in explicit fallback mode"
|
|
256
|
+
),
|
|
257
|
+
"status": "verified" if not degraded else "degraded",
|
|
258
|
+
"mode": "normal" if not degraded else "fallback",
|
|
259
|
+
"issues": issues,
|
|
260
|
+
"receipt": receipt,
|
|
261
|
+
"receiptHash": receipt_hash,
|
|
262
|
+
"receiptPath": str(path),
|
|
263
|
+
"releasePhase": release_phase,
|
|
264
|
+
"remediation": (
|
|
265
|
+
None
|
|
266
|
+
if not degraded
|
|
267
|
+
else (
|
|
268
|
+
"regenerate .harness/config/projection-receipt.json from the "
|
|
269
|
+
"project truth source before any release phase"
|
|
270
|
+
)
|
|
271
|
+
),
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
|
|
163
275
|
def resolve_execution_root(main_project: Path, raw: str | None) -> Path:
|
|
164
276
|
candidate = Path(raw).expanduser().resolve() if raw else main_project.resolve()
|
|
165
277
|
if not candidate.is_dir():
|
|
@@ -173,6 +285,46 @@ def resolve_execution_root(main_project: Path, raw: str | None) -> Path:
|
|
|
173
285
|
return root
|
|
174
286
|
|
|
175
287
|
|
|
288
|
+
def resolve_begin_execution_hint(
|
|
289
|
+
main_project: Path,
|
|
290
|
+
*,
|
|
291
|
+
requested: str | None,
|
|
292
|
+
capsule: dict[str, Any] | None,
|
|
293
|
+
cwd: Path | None = None,
|
|
294
|
+
) -> dict[str, str]:
|
|
295
|
+
"""Choose an execution root explicitly, from the capsule, or from Git cwd."""
|
|
296
|
+
if requested:
|
|
297
|
+
return {
|
|
298
|
+
"executionRoot": str(Path(requested).expanduser().resolve()),
|
|
299
|
+
"source": "explicit",
|
|
300
|
+
}
|
|
301
|
+
if isinstance(capsule, dict) and str(capsule.get("executionRoot") or "").strip():
|
|
302
|
+
return {
|
|
303
|
+
"executionRoot": str(capsule["executionRoot"]),
|
|
304
|
+
"source": "phase-capsule",
|
|
305
|
+
}
|
|
306
|
+
current = (cwd or Path.cwd()).resolve()
|
|
307
|
+
try:
|
|
308
|
+
top = _git_text(current, "rev-parse", "--show-toplevel")
|
|
309
|
+
detected = Path(top).resolve() if top else None
|
|
310
|
+
if (
|
|
311
|
+
detected is not None
|
|
312
|
+
and detected.is_dir()
|
|
313
|
+
and hp.repository_identity(detected)
|
|
314
|
+
== hp.repository_identity(main_project)
|
|
315
|
+
):
|
|
316
|
+
return {
|
|
317
|
+
"executionRoot": str(detected),
|
|
318
|
+
"source": "cwd-git-root",
|
|
319
|
+
}
|
|
320
|
+
except (OSError, RuntimeError, ValueError):
|
|
321
|
+
pass
|
|
322
|
+
return {
|
|
323
|
+
"executionRoot": str(main_project.resolve()),
|
|
324
|
+
"source": "main-project-default",
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
|
|
176
328
|
def _phase_capsule_path(change_dir: Path, phase: str, run_id: str) -> Path:
|
|
177
329
|
key = hashlib.sha256(f"{phase}\0{run_id}".encode("utf-8")).hexdigest()[:20]
|
|
178
330
|
return (
|
|
@@ -206,6 +358,37 @@ def write_phase_capsule(
|
|
|
206
358
|
return path
|
|
207
359
|
|
|
208
360
|
|
|
361
|
+
def persist_close_failure(
|
|
362
|
+
change_dir: Path,
|
|
363
|
+
phase: str,
|
|
364
|
+
run_id: str,
|
|
365
|
+
capsule: dict[str, Any] | None,
|
|
366
|
+
*,
|
|
367
|
+
status: str,
|
|
368
|
+
error: dict[str, Any],
|
|
369
|
+
) -> None:
|
|
370
|
+
"""Journal a retryable close failure before returning to the caller."""
|
|
371
|
+
if capsule is None:
|
|
372
|
+
return
|
|
373
|
+
transaction = capsule.get("closeTransaction")
|
|
374
|
+
transaction = dict(transaction) if isinstance(transaction, dict) else {}
|
|
375
|
+
transaction.update(
|
|
376
|
+
{
|
|
377
|
+
"status": status,
|
|
378
|
+
"retryable": True,
|
|
379
|
+
"guardClosed": bool(transaction.get("guardClosed")),
|
|
380
|
+
"phaseEndRecorded": bool(transaction.get("phaseEndRecorded")),
|
|
381
|
+
"leaseReleased": False,
|
|
382
|
+
"lastError": error,
|
|
383
|
+
"updatedAt": he.now_iso(),
|
|
384
|
+
}
|
|
385
|
+
)
|
|
386
|
+
capsule["closeTransaction"] = transaction
|
|
387
|
+
capsule.pop("closeStatus", None)
|
|
388
|
+
capsule.pop("closedAt", None)
|
|
389
|
+
write_phase_capsule(change_dir, phase, run_id, capsule)
|
|
390
|
+
|
|
391
|
+
|
|
209
392
|
def validate_phase_capsule(
|
|
210
393
|
capsule: dict[str, Any],
|
|
211
394
|
*,
|
|
@@ -770,6 +953,145 @@ def _ordered_unique(items: list[str]) -> list[str]:
|
|
|
770
953
|
return list(dict.fromkeys(items))
|
|
771
954
|
|
|
772
955
|
|
|
956
|
+
FINAL_SEQUENCE_CANONICAL_ORDER = (
|
|
957
|
+
"complete-review",
|
|
958
|
+
"code-freeze",
|
|
959
|
+
"unit-test-full",
|
|
960
|
+
"delta-review",
|
|
961
|
+
"submit-reuse",
|
|
962
|
+
"remote-candidate-ci",
|
|
963
|
+
"archive",
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
def _sequence_slug(value: str) -> str:
|
|
968
|
+
expanded = re.sub(r"(?<=[a-z0-9])(?=[A-Z])", "-", value.strip())
|
|
969
|
+
return re.sub(r"[^a-z0-9]+", "-", expanded.lower()).strip("-")
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
def compile_final_sequence_dag(
|
|
973
|
+
base_dag: dict[str, Any],
|
|
974
|
+
sequence: list[Any],
|
|
975
|
+
) -> dict[str, Any]:
|
|
976
|
+
"""Compile a declared release sequence into an executable, versioned DAG."""
|
|
977
|
+
raw_nodes = base_dag.get("nodes")
|
|
978
|
+
raw_edges = base_dag.get("edges")
|
|
979
|
+
if not isinstance(raw_nodes, list) or not isinstance(raw_edges, list):
|
|
980
|
+
raise ValueError("base requiredGateDag must contain nodes and edges arrays")
|
|
981
|
+
if not sequence:
|
|
982
|
+
raise ValueError("finalSequence must not be empty")
|
|
983
|
+
|
|
984
|
+
entries: list[dict[str, Any]] = []
|
|
985
|
+
seen: set[str] = set()
|
|
986
|
+
for index, raw in enumerate(sequence):
|
|
987
|
+
if isinstance(raw, str):
|
|
988
|
+
name = raw
|
|
989
|
+
metadata: dict[str, Any] = {}
|
|
990
|
+
elif isinstance(raw, dict):
|
|
991
|
+
name = str(raw.get("id") or raw.get("name") or raw.get("step") or "")
|
|
992
|
+
metadata = {
|
|
993
|
+
key: value
|
|
994
|
+
for key, value in raw.items()
|
|
995
|
+
if key not in {"id", "name", "step", "dependsOn", "kind"}
|
|
996
|
+
}
|
|
997
|
+
else:
|
|
998
|
+
raise ValueError(f"finalSequence[{index}] must be a string or object")
|
|
999
|
+
slug = _sequence_slug(name)
|
|
1000
|
+
if not slug:
|
|
1001
|
+
raise ValueError(f"finalSequence[{index}] requires a non-empty name")
|
|
1002
|
+
if slug in seen:
|
|
1003
|
+
raise ValueError(f"duplicate finalSequence step: {slug}")
|
|
1004
|
+
seen.add(slug)
|
|
1005
|
+
entries.append({"slug": slug, "metadata": metadata})
|
|
1006
|
+
|
|
1007
|
+
positions = {entry["slug"]: index for index, entry in enumerate(entries)}
|
|
1008
|
+
present_order = [
|
|
1009
|
+
name for name in FINAL_SEQUENCE_CANONICAL_ORDER if name in positions
|
|
1010
|
+
]
|
|
1011
|
+
for left, right in zip(present_order, present_order[1:]):
|
|
1012
|
+
if positions[left] > positions[right]:
|
|
1013
|
+
raise ValueError(
|
|
1014
|
+
f"finalSequence must place {left} before {right}; "
|
|
1015
|
+
"code-freeze must precede unit-test-full"
|
|
1016
|
+
)
|
|
1017
|
+
if "archive" in positions and positions["archive"] != len(entries) - 1:
|
|
1018
|
+
raise ValueError("finalSequence archive step must be last")
|
|
1019
|
+
|
|
1020
|
+
nodes = [dict(node) for node in raw_nodes if isinstance(node, dict)]
|
|
1021
|
+
edges = [dict(edge) for edge in raw_edges if isinstance(edge, dict)]
|
|
1022
|
+
base_ids = {
|
|
1023
|
+
str(node.get("id") or "")
|
|
1024
|
+
for node in nodes
|
|
1025
|
+
if str(node.get("id") or "")
|
|
1026
|
+
}
|
|
1027
|
+
depended_on = {
|
|
1028
|
+
str(dependency)
|
|
1029
|
+
for node in nodes
|
|
1030
|
+
for dependency in (node.get("dependsOn") or [])
|
|
1031
|
+
if isinstance(dependency, str)
|
|
1032
|
+
}
|
|
1033
|
+
base_terminals = sorted(base_ids - depended_on)
|
|
1034
|
+
|
|
1035
|
+
sequence_ids: list[str] = []
|
|
1036
|
+
predecessor_ids = base_terminals
|
|
1037
|
+
freeze_id = "sequence:code-freeze"
|
|
1038
|
+
for index, entry in enumerate(entries):
|
|
1039
|
+
node_id = f"sequence:{entry['slug']}"
|
|
1040
|
+
sequence_ids.append(node_id)
|
|
1041
|
+
node = {
|
|
1042
|
+
"id": node_id,
|
|
1043
|
+
"kind": "sequence",
|
|
1044
|
+
"phase": "archive",
|
|
1045
|
+
"sequenceIndex": index,
|
|
1046
|
+
"dependsOn": list(predecessor_ids),
|
|
1047
|
+
"subjectBound": True,
|
|
1048
|
+
"invalidatedByProductChange": (
|
|
1049
|
+
freeze_id in sequence_ids or node_id == freeze_id
|
|
1050
|
+
),
|
|
1051
|
+
**entry["metadata"],
|
|
1052
|
+
}
|
|
1053
|
+
nodes.append(node)
|
|
1054
|
+
edges.extend(
|
|
1055
|
+
{"from": dependency, "to": node_id}
|
|
1056
|
+
for dependency in predecessor_ids
|
|
1057
|
+
)
|
|
1058
|
+
predecessor_ids = [node_id]
|
|
1059
|
+
|
|
1060
|
+
return {
|
|
1061
|
+
"schemaVersion": 2,
|
|
1062
|
+
"nodes": nodes,
|
|
1063
|
+
"edges": edges,
|
|
1064
|
+
"finalSequence": {
|
|
1065
|
+
"schemaVersion": 1,
|
|
1066
|
+
"nodeIds": sequence_ids,
|
|
1067
|
+
"freezeNodeId": (
|
|
1068
|
+
freeze_id if freeze_id in sequence_ids else None
|
|
1069
|
+
),
|
|
1070
|
+
"terminalNodeId": sequence_ids[-1],
|
|
1071
|
+
},
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
def _load_configured_final_sequence(
|
|
1076
|
+
project: Path,
|
|
1077
|
+
change_dir: Path,
|
|
1078
|
+
) -> list[Any] | None:
|
|
1079
|
+
candidates = (
|
|
1080
|
+
change_dir / "meta" / "final-sequence.json",
|
|
1081
|
+
project / ".harness" / "config" / "final-sequence.json",
|
|
1082
|
+
)
|
|
1083
|
+
for path in candidates:
|
|
1084
|
+
if not path.is_file():
|
|
1085
|
+
continue
|
|
1086
|
+
raw = _read_json(path)
|
|
1087
|
+
if isinstance(raw, list):
|
|
1088
|
+
return raw
|
|
1089
|
+
if isinstance(raw, dict) and isinstance(raw.get("finalSequence"), list):
|
|
1090
|
+
return raw["finalSequence"]
|
|
1091
|
+
raise ValueError(f"final sequence configuration is invalid: {path}")
|
|
1092
|
+
return None
|
|
1093
|
+
|
|
1094
|
+
|
|
773
1095
|
def _apply_required_gate_contract(
|
|
774
1096
|
payload: dict[str, Any],
|
|
775
1097
|
workflow: dict[str, Any],
|
|
@@ -949,7 +1271,15 @@ def classify_risk(
|
|
|
949
1271
|
}
|
|
950
1272
|
if stage == "post-run":
|
|
951
1273
|
_write_json(change_dir / "meta" / "risk-classification.json", payload)
|
|
952
|
-
|
|
1274
|
+
result = _apply_required_gate_contract(payload, workflow, capabilities)
|
|
1275
|
+
configured_sequence = _load_configured_final_sequence(main_project, change_dir)
|
|
1276
|
+
if configured_sequence is not None:
|
|
1277
|
+
result["requiredGateDag"] = compile_final_sequence_dag(
|
|
1278
|
+
result["requiredGateDag"],
|
|
1279
|
+
configured_sequence,
|
|
1280
|
+
)
|
|
1281
|
+
result["finalSequence"] = result["requiredGateDag"]["finalSequence"]
|
|
1282
|
+
return result
|
|
953
1283
|
|
|
954
1284
|
|
|
955
1285
|
def _stage_decisions_for_tier(
|
|
@@ -993,14 +1323,35 @@ def apply_tier_override(
|
|
|
993
1323
|
payload["conditionalStages"] = list(tier_policy["conditionalStages"])
|
|
994
1324
|
payload["stageDecisions"] = _stage_decisions_for_tier(workflow, tier, signals)
|
|
995
1325
|
payload["tierOverride"] = {"tier": tier, "by": override_by, "at": now}
|
|
996
|
-
|
|
1326
|
+
result = _apply_required_gate_contract(
|
|
997
1327
|
payload, workflow, list(payload.get("capabilities") or [])
|
|
998
1328
|
)
|
|
1329
|
+
final_sequence = payload.get("finalSequence")
|
|
1330
|
+
node_ids = (
|
|
1331
|
+
final_sequence.get("nodeIds")
|
|
1332
|
+
if isinstance(final_sequence, dict)
|
|
1333
|
+
else None
|
|
1334
|
+
)
|
|
1335
|
+
if isinstance(node_ids, list) and node_ids:
|
|
1336
|
+
sequence = [
|
|
1337
|
+
str(node_id).removeprefix("sequence:")
|
|
1338
|
+
for node_id in node_ids
|
|
1339
|
+
if str(node_id).startswith("sequence:")
|
|
1340
|
+
]
|
|
1341
|
+
if sequence:
|
|
1342
|
+
result["requiredGateDag"] = compile_final_sequence_dag(
|
|
1343
|
+
result["requiredGateDag"],
|
|
1344
|
+
sequence,
|
|
1345
|
+
)
|
|
1346
|
+
result["finalSequence"] = result["requiredGateDag"][
|
|
1347
|
+
"finalSequence"
|
|
1348
|
+
]
|
|
1349
|
+
return result
|
|
999
1350
|
|
|
1000
1351
|
|
|
1001
1352
|
def gate_policy_document(payload: dict[str, Any]) -> dict[str, Any]:
|
|
1002
1353
|
"""Cross-change contract: meta/gate-policy.json (schemaVersion=1)."""
|
|
1003
|
-
|
|
1354
|
+
document = {
|
|
1004
1355
|
"schemaVersion": 1,
|
|
1005
1356
|
"tier": payload["tier"],
|
|
1006
1357
|
"source": payload["source"],
|
|
@@ -1016,6 +1367,12 @@ def gate_policy_document(payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
1016
1367
|
"classifiedAt": payload.get("classifiedAt") or hc.now_iso(),
|
|
1017
1368
|
"tierOverride": payload.get("tierOverride"),
|
|
1018
1369
|
}
|
|
1370
|
+
if isinstance(payload.get("finalSequence"), dict):
|
|
1371
|
+
document["finalSequence"] = dict(payload["finalSequence"])
|
|
1372
|
+
for optional in ("candidateVerification", "releasePolicy"):
|
|
1373
|
+
if isinstance(payload.get(optional), dict):
|
|
1374
|
+
document[optional] = dict(payload[optional])
|
|
1375
|
+
return document
|
|
1019
1376
|
|
|
1020
1377
|
|
|
1021
1378
|
def classify_defaults(
|
|
@@ -1352,6 +1709,18 @@ def cmd_begin(args: argparse.Namespace) -> int:
|
|
|
1352
1709
|
except (OSError, json.JSONDecodeError, ValueError) as exc:
|
|
1353
1710
|
return emit_error("BUNDLE_IDENTITY_INVALID", str(exc), as_json=as_json)
|
|
1354
1711
|
executor_tool = executor_tool or str(identity.get("adapter") or "") or None
|
|
1712
|
+
projection = evaluate_projection_gate(project, args.phase)
|
|
1713
|
+
if not projection.get("ok"):
|
|
1714
|
+
return emit_error(
|
|
1715
|
+
str(projection.get("code") or "PROJECTION_BLOCKED"),
|
|
1716
|
+
str(projection.get("message") or "projection gate blocked phase"),
|
|
1717
|
+
as_json=as_json,
|
|
1718
|
+
extra={
|
|
1719
|
+
key: value
|
|
1720
|
+
for key, value in projection.items()
|
|
1721
|
+
if key not in {"ok", "code", "message"}
|
|
1722
|
+
},
|
|
1723
|
+
)
|
|
1355
1724
|
explicit_run_id = args.run_id or os.environ.get("HUNTER_HARNESS_RUN_ID")
|
|
1356
1725
|
current_lease = hc.inspect_lease(project, resolved["changeId"])
|
|
1357
1726
|
run_id = explicit_run_id or (
|
|
@@ -1363,11 +1732,16 @@ def cmd_begin(args: argparse.Namespace) -> int:
|
|
|
1363
1732
|
capsule = load_phase_capsule(change_dir, args.phase, run_id)
|
|
1364
1733
|
except ValueError as exc:
|
|
1365
1734
|
return emit_error("PHASE_CAPSULE_INVALID", str(exc), as_json=as_json)
|
|
1366
|
-
execution_hint =
|
|
1367
|
-
|
|
1368
|
-
|
|
1735
|
+
execution_hint = resolve_begin_execution_hint(
|
|
1736
|
+
project,
|
|
1737
|
+
requested=args.project,
|
|
1738
|
+
capsule=capsule,
|
|
1739
|
+
)
|
|
1369
1740
|
try:
|
|
1370
|
-
execution_root = resolve_execution_root(
|
|
1741
|
+
execution_root = resolve_execution_root(
|
|
1742
|
+
project,
|
|
1743
|
+
execution_hint["executionRoot"],
|
|
1744
|
+
)
|
|
1371
1745
|
except ValueError as exc:
|
|
1372
1746
|
return emit_error("EXECUTION_ROOT_INVALID", str(exc), as_json=as_json)
|
|
1373
1747
|
if capsule is not None:
|
|
@@ -1436,6 +1810,7 @@ def cmd_begin(args: argparse.Namespace) -> int:
|
|
|
1436
1810
|
"baseCommit": base_commit or current_head,
|
|
1437
1811
|
"currentHead": current_head,
|
|
1438
1812
|
"createdAt": he.now_iso(),
|
|
1813
|
+
"projectionReceipt": projection,
|
|
1439
1814
|
}
|
|
1440
1815
|
write_phase_capsule(change_dir, args.phase, run_id, capsule)
|
|
1441
1816
|
event_result = {"ok": True, "skipped": True, "reason": "already-recorded"} \
|
|
@@ -1472,6 +1847,8 @@ def cmd_begin(args: argparse.Namespace) -> int:
|
|
|
1472
1847
|
"event": event_result,
|
|
1473
1848
|
"testGuard": guard_result,
|
|
1474
1849
|
"policySchemaVersion": policy.get("schemaVersion"),
|
|
1850
|
+
"projection": projection,
|
|
1851
|
+
"executionRootSource": execution_hint["source"],
|
|
1475
1852
|
}
|
|
1476
1853
|
emit(payload, as_json=as_json)
|
|
1477
1854
|
return 0
|
|
@@ -1529,6 +1906,18 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
1529
1906
|
if args.project:
|
|
1530
1907
|
requested_root = resolve_execution_root(project, args.project)
|
|
1531
1908
|
if requested_root != execution_root:
|
|
1909
|
+
persist_close_failure(
|
|
1910
|
+
change_dir,
|
|
1911
|
+
args.phase,
|
|
1912
|
+
run_id,
|
|
1913
|
+
capsule,
|
|
1914
|
+
status="ROOT_VALIDATION_FAILED",
|
|
1915
|
+
error={
|
|
1916
|
+
"code": "EXECUTION_ROOT_MISMATCH",
|
|
1917
|
+
"storedExecutionRoot": str(execution_root),
|
|
1918
|
+
"requestedExecutionRoot": str(requested_root),
|
|
1919
|
+
},
|
|
1920
|
+
)
|
|
1532
1921
|
return emit_error(
|
|
1533
1922
|
"EXECUTION_ROOT_MISMATCH",
|
|
1534
1923
|
"close must use the execution root captured at begin",
|
|
@@ -1536,9 +1925,24 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
1536
1925
|
extra={
|
|
1537
1926
|
"storedExecutionRoot": str(execution_root),
|
|
1538
1927
|
"requestedExecutionRoot": str(requested_root),
|
|
1928
|
+
"recoveryAction": (
|
|
1929
|
+
"retry close from the stored execution root or "
|
|
1930
|
+
"pass --project with storedExecutionRoot"
|
|
1931
|
+
),
|
|
1539
1932
|
},
|
|
1540
1933
|
)
|
|
1541
1934
|
except ValueError as exc:
|
|
1935
|
+
persist_close_failure(
|
|
1936
|
+
change_dir,
|
|
1937
|
+
args.phase,
|
|
1938
|
+
run_id,
|
|
1939
|
+
capsule,
|
|
1940
|
+
status="ROOT_VALIDATION_FAILED",
|
|
1941
|
+
error={
|
|
1942
|
+
"code": "EXECUTION_ROOT_INVALID",
|
|
1943
|
+
"message": str(exc),
|
|
1944
|
+
},
|
|
1945
|
+
)
|
|
1542
1946
|
return emit_error("EXECUTION_ROOT_INVALID", str(exc), as_json=as_json)
|
|
1543
1947
|
else:
|
|
1544
1948
|
try:
|
|
@@ -1558,13 +1962,85 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
1558
1962
|
allow_head_advance=args.phase in {"run", "submit", "merge"},
|
|
1559
1963
|
)
|
|
1560
1964
|
except ValueError as exc:
|
|
1965
|
+
persist_close_failure(
|
|
1966
|
+
change_dir,
|
|
1967
|
+
args.phase,
|
|
1968
|
+
run_id,
|
|
1969
|
+
capsule,
|
|
1970
|
+
status="CAPSULE_VALIDATION_FAILED",
|
|
1971
|
+
error={
|
|
1972
|
+
"code": "PHASE_CAPSULE_MISMATCH",
|
|
1973
|
+
"message": str(exc),
|
|
1974
|
+
},
|
|
1975
|
+
)
|
|
1561
1976
|
return emit_error("PHASE_CAPSULE_MISMATCH", str(exc), as_json=as_json)
|
|
1562
1977
|
|
|
1978
|
+
projection = evaluate_projection_gate(project, args.phase)
|
|
1979
|
+
if not projection.get("ok"):
|
|
1980
|
+
persist_close_failure(
|
|
1981
|
+
change_dir,
|
|
1982
|
+
args.phase,
|
|
1983
|
+
run_id,
|
|
1984
|
+
capsule,
|
|
1985
|
+
status="PROJECTION_VALIDATION_FAILED",
|
|
1986
|
+
error=projection,
|
|
1987
|
+
)
|
|
1988
|
+
return emit_error(
|
|
1989
|
+
str(projection.get("code") or "PROJECTION_BLOCKED"),
|
|
1990
|
+
str(projection.get("message") or "projection gate blocked close"),
|
|
1991
|
+
as_json=as_json,
|
|
1992
|
+
extra={
|
|
1993
|
+
key: value
|
|
1994
|
+
for key, value in projection.items()
|
|
1995
|
+
if key not in {"ok", "code", "message"}
|
|
1996
|
+
},
|
|
1997
|
+
)
|
|
1998
|
+
captured_projection = (
|
|
1999
|
+
capsule.get("projectionReceipt")
|
|
2000
|
+
if isinstance(capsule, dict)
|
|
2001
|
+
else None
|
|
2002
|
+
)
|
|
2003
|
+
if (
|
|
2004
|
+
isinstance(captured_projection, dict)
|
|
2005
|
+
and captured_projection.get("receiptHash")
|
|
2006
|
+
and captured_projection.get("receiptHash")
|
|
2007
|
+
!= projection.get("receiptHash")
|
|
2008
|
+
and args.phase in {"submit", "archive", "release", "deploy"}
|
|
2009
|
+
):
|
|
2010
|
+
mismatch = {
|
|
2011
|
+
"code": "PROJECTION_CHANGED_DURING_PHASE",
|
|
2012
|
+
"message": "projection receipt changed after phase begin",
|
|
2013
|
+
"capturedReceiptHash": captured_projection.get("receiptHash"),
|
|
2014
|
+
"currentReceiptHash": projection.get("receiptHash"),
|
|
2015
|
+
}
|
|
2016
|
+
persist_close_failure(
|
|
2017
|
+
change_dir,
|
|
2018
|
+
args.phase,
|
|
2019
|
+
run_id,
|
|
2020
|
+
capsule,
|
|
2021
|
+
status="PROJECTION_VALIDATION_FAILED",
|
|
2022
|
+
error=mismatch,
|
|
2023
|
+
)
|
|
2024
|
+
return emit_error(
|
|
2025
|
+
mismatch["code"],
|
|
2026
|
+
mismatch["message"],
|
|
2027
|
+
as_json=as_json,
|
|
2028
|
+
extra=mismatch,
|
|
2029
|
+
)
|
|
2030
|
+
|
|
1563
2031
|
ledger_result = validate_ledger_for_phase_close(
|
|
1564
2032
|
change_dir, args.phase, policy, execution_root=execution_root,
|
|
1565
2033
|
phase_status=args.status,
|
|
1566
2034
|
)
|
|
1567
2035
|
if not ledger_result.get("ok") and args.phase in {"run", "test", "package"}:
|
|
2036
|
+
persist_close_failure(
|
|
2037
|
+
change_dir,
|
|
2038
|
+
args.phase,
|
|
2039
|
+
run_id,
|
|
2040
|
+
capsule,
|
|
2041
|
+
status="LEDGER_VALIDATION_FAILED",
|
|
2042
|
+
error=ledger_result,
|
|
2043
|
+
)
|
|
1568
2044
|
return emit_error(
|
|
1569
2045
|
str(ledger_result.get("code", "LEDGER_INVALID")),
|
|
1570
2046
|
str(ledger_result.get("message", "ledger validation failed")),
|
|
@@ -1576,6 +2052,14 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
1576
2052
|
if args.phase in {"run", "test"}:
|
|
1577
2053
|
coverage = _validate_scenario_coverage(change_dir)
|
|
1578
2054
|
if not coverage.get("ok"):
|
|
2055
|
+
persist_close_failure(
|
|
2056
|
+
change_dir,
|
|
2057
|
+
args.phase,
|
|
2058
|
+
run_id,
|
|
2059
|
+
capsule,
|
|
2060
|
+
status="SCENARIO_VALIDATION_FAILED",
|
|
2061
|
+
error=coverage,
|
|
2062
|
+
)
|
|
1579
2063
|
return emit_error(
|
|
1580
2064
|
str(coverage.get("code", "SCENARIO_COVERAGE_FAILED")),
|
|
1581
2065
|
str(coverage.get("message", "scenario coverage validation failed")),
|