prizmkit 1.1.131 → 1.1.134
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/bundled/VERSION.json +3 -3
- package/bundled/adapters/claude/command-adapter.js +0 -2
- package/bundled/dev-pipeline/assets/skill-subagent-integration.md +11 -6
- package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +2 -9
- package/bundled/dev-pipeline/prizmkit_runtime/processes.py +21 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +104 -17
- package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +3 -2
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +15 -13
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +3 -13
- package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +5 -4
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +1 -1
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +4 -4
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +9 -9
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +4 -7
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +3 -3
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-commit-report.md +2 -2
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-manual-verification.md +5 -10
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +5 -5
- package/bundled/dev-pipeline/templates/sections/bugfix-session-status.md +1 -1
- package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +1 -1
- package/bundled/dev-pipeline/templates/sections/headless-commit-authorization.md +2 -3
- package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +2 -2
- package/bundled/dev-pipeline/templates/sections/phase-commit.md +2 -2
- package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +5 -5
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +5 -5
- package/bundled/dev-pipeline/templates/sections/refactor-phase-commit-report.md +1 -1
- package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +5 -5
- package/bundled/dev-pipeline/templates/sections/refactor-reminders.md +1 -1
- package/bundled/dev-pipeline/templates/sections/subagent-timeout-recovery.md +1 -1
- package/bundled/dev-pipeline/templates/sections/task-contract.md +4 -0
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +39 -26
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +7 -10
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +3 -3
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +156 -5
- package/bundled/dev-pipeline/tests/test_unified_cli.py +34 -7
- package/bundled/skills/_metadata.json +3 -3
- package/bundled/skills/prizmkit/references/workflow-state-protocol.md +3 -3
- package/bundled/skills/prizmkit-code-review/SKILL.md +22 -6
- package/bundled/skills/prizmkit-code-review/references/independent-code-review.md +243 -0
- package/bundled/skills/prizmkit-code-review/references/review-report-template.md +41 -0
- package/bundled/skills/prizmkit-code-review/references/workflow-state-protocol.md +3 -3
- package/bundled/skills/prizmkit-code-review/scripts/render_review_report.py +93 -1
- package/bundled/skills/prizmkit-committer/SKILL.md +6 -13
- package/bundled/skills/prizmkit-committer/references/workflow-state-protocol.md +3 -3
- package/bundled/skills/prizmkit-implement/references/workflow-state-protocol.md +3 -3
- package/bundled/skills/prizmkit-plan/SKILL.md +17 -3
- package/bundled/skills/prizmkit-plan/assets/plan-template.md +18 -0
- package/bundled/skills/prizmkit-plan/references/independent-plan-review.md +242 -0
- package/bundled/skills/prizmkit-plan/references/review-plan-spec-loop.md +4 -4
- package/bundled/skills/prizmkit-plan/references/workflow-state-protocol.md +3 -3
- package/bundled/skills/prizmkit-retrospective/references/workflow-state-protocol.md +3 -3
- package/bundled/skills/prizmkit-test/SKILL.md +25 -1
- package/bundled/skills/prizmkit-test/assets/authoritative-records.schema.json +63 -12
- package/bundled/skills/prizmkit-test/assets/evidence-manifest.schema.json +1 -1
- package/bundled/skills/prizmkit-test/assets/evidence-package-template.json +13 -0
- package/bundled/skills/prizmkit-test/references/evidence-protocol.md +6 -1
- package/bundled/skills/prizmkit-test/references/evidence-request-protocol.md +8 -1
- package/bundled/skills/prizmkit-test/references/test-generation-steps.md +4 -1
- package/bundled/skills/prizmkit-test/references/trusted-evidence-execution.md +15 -0
- package/bundled/skills/prizmkit-test/references/workflow-state-protocol.md +3 -3
- package/bundled/skills/prizmkit-test/scripts/build_test_evidence.py +744 -23
- package/bundled/skills/prizmkit-test/scripts/validate_test_evidence.py +194 -15
- package/bundled/skills/prizmkit-workflow/SKILL.md +1 -1
- package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +3 -3
- package/package.json +1 -1
|
@@ -38,6 +38,7 @@ LAYER_ORDER = {
|
|
|
38
38
|
"regression-ring": 4,
|
|
39
39
|
}
|
|
40
40
|
FINAL_RECORDS = {"validation.json", "verdict.json", "test-report.md"}
|
|
41
|
+
ATTESTATION_MUTABLE = {"validation.json"}
|
|
41
42
|
COMMON_RECORDS = {
|
|
42
43
|
"change-classification.json", "scope.json", "target-inventory.json",
|
|
43
44
|
"environment.json", "executions.json", "verdict.json", "validation.json",
|
|
@@ -54,9 +55,13 @@ BLOCKED_EXTERNAL_CLASSES = {"production", "unknown"}
|
|
|
54
55
|
SNAPSHOT_VOLATILE_ROOTS = (
|
|
55
56
|
".prizmkit/state",
|
|
56
57
|
".prizmkit/test/evidence",
|
|
58
|
+
".prizmkit/" + "dev-" + "pipeline",
|
|
57
59
|
".claude/worktrees",
|
|
60
|
+
".agents/worktrees",
|
|
61
|
+
".codebuddy/worktrees",
|
|
62
|
+
".codex/worktrees",
|
|
58
63
|
)
|
|
59
|
-
SNAPSHOT_VOLATILE_NAMES = {".git", "__pycache__"}
|
|
64
|
+
SNAPSHOT_VOLATILE_NAMES = {".git", "__pycache__", ".pytest_cache", ".mypy_cache"}
|
|
60
65
|
RISK_CONFLICT_PATTERNS = {
|
|
61
66
|
"permission": re.compile(r"auth|tenant|role|permission|access|acl|rbac", re.I),
|
|
62
67
|
"concurrency": re.compile(r"lock|shared[ _-]?state|worker|queue|concurr|parallel|race", re.I),
|
|
@@ -219,6 +224,18 @@ def validate_schema(
|
|
|
219
224
|
"""Validate the strict JSON-Schema subset used by skill-owned schemas."""
|
|
220
225
|
schema = resolve_ref(schema, root_schema)
|
|
221
226
|
|
|
227
|
+
one_of = schema.get("oneOf")
|
|
228
|
+
if isinstance(one_of, list):
|
|
229
|
+
matching = 0
|
|
230
|
+
for branch in one_of:
|
|
231
|
+
if not isinstance(branch, dict):
|
|
232
|
+
continue
|
|
233
|
+
branch_errors: list[str] = []
|
|
234
|
+
validate_schema(value, branch, root_schema, location, branch_errors)
|
|
235
|
+
if not branch_errors:
|
|
236
|
+
matching += 1
|
|
237
|
+
require(matching == 1, f"{location}: value must match exactly one schema branch", errors)
|
|
238
|
+
|
|
222
239
|
for child in schema.get("allOf", []):
|
|
223
240
|
if isinstance(child, dict):
|
|
224
241
|
validate_schema(value, child, root_schema, location, errors)
|
|
@@ -341,6 +358,31 @@ def require_manifest_entry(
|
|
|
341
358
|
return path
|
|
342
359
|
|
|
343
360
|
|
|
361
|
+
def validate_differential_na(
|
|
362
|
+
value: Any,
|
|
363
|
+
location: str,
|
|
364
|
+
errors: list[str],
|
|
365
|
+
) -> None:
|
|
366
|
+
"""Validate the differential-only N/A shape without weakening risk-cell N/A."""
|
|
367
|
+
if not isinstance(value, dict):
|
|
368
|
+
errors.append(f"{location}: differential N/A must be an object")
|
|
369
|
+
return
|
|
370
|
+
required = {"reason", "rationale", "evidence", "considered_signals", "conflicts"}
|
|
371
|
+
require(set(value) == required, f"{location}: differential N/A fields are incomplete", errors)
|
|
372
|
+
require(value.get("reason") in {"new-behavior", "textual-contract"}, f"{location}: invalid differential N/A reason", errors)
|
|
373
|
+
rationale = value.get("rationale")
|
|
374
|
+
require(isinstance(rationale, str) and len(rationale.strip()) >= 16, f"{location}: differential N/A rationale is too short", errors)
|
|
375
|
+
if value.get("reason") == "new-behavior" and isinstance(rationale, str):
|
|
376
|
+
require("no stable pre-change observable" in rationale.lower(), f"{location}: new behavior N/A lacks baseline rationale", errors)
|
|
377
|
+
if value.get("reason") == "textual-contract" and isinstance(rationale, str):
|
|
378
|
+
require("textual" in rationale.lower(), f"{location}: textual contract N/A lacks textual rationale", errors)
|
|
379
|
+
evidence = value.get("evidence")
|
|
380
|
+
require(isinstance(evidence, list) and bool(evidence) and all(isinstance(item, str) and item.strip() for item in evidence), f"{location}: differential N/A needs evidence strings", errors)
|
|
381
|
+
signals = value.get("considered_signals")
|
|
382
|
+
require(isinstance(signals, list) and bool(signals) and all(isinstance(item, str) and item.strip() for item in signals), f"{location}: differential N/A needs considered signals", errors)
|
|
383
|
+
require(isinstance(value.get("conflicts"), list), f"{location}: differential N/A conflicts must be an array", errors)
|
|
384
|
+
|
|
385
|
+
|
|
344
386
|
def structured_na(
|
|
345
387
|
value: Any,
|
|
346
388
|
records_schema: dict[str, Any],
|
|
@@ -503,6 +545,25 @@ def validate_manifest(
|
|
|
503
545
|
return entry_map
|
|
504
546
|
|
|
505
547
|
|
|
548
|
+
def get_captured_deleted_paths(root: Path) -> set[str]:
|
|
549
|
+
capture_path = root / "change-capture.json"
|
|
550
|
+
if not capture_path.is_file():
|
|
551
|
+
return set()
|
|
552
|
+
try:
|
|
553
|
+
capture = load_json(capture_path)
|
|
554
|
+
except EvidenceError:
|
|
555
|
+
return set()
|
|
556
|
+
if not isinstance(capture, dict):
|
|
557
|
+
return set()
|
|
558
|
+
return {
|
|
559
|
+
item["path"]
|
|
560
|
+
for item in capture.get("changed_files", [])
|
|
561
|
+
if isinstance(item, dict)
|
|
562
|
+
and isinstance(item.get("path"), str)
|
|
563
|
+
and (item.get("status", "").strip() == "D" or item.get("role") == "source")
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
|
|
506
567
|
def validate_inventory(
|
|
507
568
|
root: Path,
|
|
508
569
|
project_root: Path,
|
|
@@ -569,6 +630,7 @@ def validate_inventory(
|
|
|
569
630
|
require(plan.get("plan_inputs") == plan_inputs, "test plan inputs do not match target inventory", errors)
|
|
570
631
|
|
|
571
632
|
patch = require_manifest_entry("source-change.patch", entry_map, root, errors, "source-change patch")
|
|
633
|
+
captured_deleted_paths = get_captured_deleted_paths(root)
|
|
572
634
|
if patch is not None:
|
|
573
635
|
require(manifest.get("working_diff_sha256") == file_sha256(patch), "working_diff_sha256 does not match source-change.patch", errors)
|
|
574
636
|
|
|
@@ -579,7 +641,19 @@ def validate_inventory(
|
|
|
579
641
|
request = load_json(request_path)
|
|
580
642
|
if isinstance(request, dict):
|
|
581
643
|
require(request.get("changed_files") == inventory.get("changed_files"), "inventory request changed_files mismatch", errors)
|
|
582
|
-
|
|
644
|
+
request_roots = request.get("module_roots")
|
|
645
|
+
normalized_request_roots = []
|
|
646
|
+
if isinstance(request_roots, list):
|
|
647
|
+
for item in request_roots:
|
|
648
|
+
if isinstance(item, str):
|
|
649
|
+
normalized_request_roots.append(item)
|
|
650
|
+
elif isinstance(item, dict) and isinstance(item.get("path"), str):
|
|
651
|
+
normalized_request_roots.append(item["path"])
|
|
652
|
+
require(
|
|
653
|
+
normalized_request_roots == inventory.get("module_roots"),
|
|
654
|
+
"inventory request module_roots mismatch",
|
|
655
|
+
errors,
|
|
656
|
+
)
|
|
583
657
|
require(request.get("exclusions") == scope.get("exclusions"), "inventory request exclusions mismatch", errors)
|
|
584
658
|
require(request.get("plan_inputs") == plan_inputs, "inventory request plan_inputs mismatch", errors)
|
|
585
659
|
else:
|
|
@@ -594,6 +668,7 @@ def validate_scope(
|
|
|
594
668
|
inventory_by_category: dict[str, dict[str, str]],
|
|
595
669
|
project_root: Path,
|
|
596
670
|
evidence_root: Path,
|
|
671
|
+
captured_deleted_paths: set[str],
|
|
597
672
|
errors: list[str],
|
|
598
673
|
) -> None:
|
|
599
674
|
changed_files = scope.get("changed_files")
|
|
@@ -614,8 +689,14 @@ def validate_scope(
|
|
|
614
689
|
if relative in exclusion_paths:
|
|
615
690
|
require(relative not in inventory_paths, f"excluded changed file is also inventoried: {relative}", errors)
|
|
616
691
|
else:
|
|
617
|
-
|
|
618
|
-
|
|
692
|
+
candidate = safe_path(project_root, relative, errors, "changed file")
|
|
693
|
+
deleted = relative in captured_deleted_paths
|
|
694
|
+
require(
|
|
695
|
+
deleted or (candidate is not None and candidate.is_file()),
|
|
696
|
+
f"changed file is not live: {relative}",
|
|
697
|
+
errors,
|
|
698
|
+
)
|
|
699
|
+
require(relative in inventory_paths or deleted, f"changed file is neither inventoried nor excluded: {relative}", errors)
|
|
619
700
|
|
|
620
701
|
roots = scope.get("module_roots")
|
|
621
702
|
require(isinstance(roots, list) and bool(roots), "scope module_roots is empty", errors)
|
|
@@ -695,7 +776,13 @@ def validate_requests_and_receipts(
|
|
|
695
776
|
if not isinstance(request, dict):
|
|
696
777
|
errors.append(f"request must be an object: {relative}")
|
|
697
778
|
continue
|
|
698
|
-
if "
|
|
779
|
+
if request.get("method") == "not-applicable":
|
|
780
|
+
validate_definition(request, "differentialRequest", records_schema, relative, errors)
|
|
781
|
+
not_applicable = request.get("not_applicable")
|
|
782
|
+
if isinstance(not_applicable, dict):
|
|
783
|
+
validate_differential_na(not_applicable, f"{relative}.not_applicable", errors)
|
|
784
|
+
differential_requests[relative] = request
|
|
785
|
+
elif "execution_request" in request:
|
|
699
786
|
validate_definition(request, "differentialRequest", records_schema, relative, errors)
|
|
700
787
|
nested = request.get("execution_request")
|
|
701
788
|
if isinstance(nested, dict):
|
|
@@ -707,6 +794,17 @@ def validate_requests_and_receipts(
|
|
|
707
794
|
validate_execution_request_semantics(request, relative, errors)
|
|
708
795
|
validate_external_targets(request.get("external_targets"), f"{relative}.external_targets", errors)
|
|
709
796
|
execution_requests[relative] = request
|
|
797
|
+
elif set(request) == {
|
|
798
|
+
"request_version", "change_class", "artifact_dir",
|
|
799
|
+
} or (
|
|
800
|
+
"request_version" in request
|
|
801
|
+
and "change_class" in request
|
|
802
|
+
and set(request) <= {
|
|
803
|
+
"request_version", "change_class", "baseline_commit", "scope_path",
|
|
804
|
+
"environment_path", "artifact_dir", "blockers", "repair_scope",
|
|
805
|
+
}
|
|
806
|
+
):
|
|
807
|
+
require(request.get("request_version", "1.0") == "1.0", f"unsupported finalize request version: {relative}", errors)
|
|
710
808
|
elif set(request) == {
|
|
711
809
|
"request_version", "categories", "changed_files", "module_roots",
|
|
712
810
|
"exclusions", "discovery_evidence", "plan_inputs",
|
|
@@ -922,12 +1020,18 @@ def validate_plan_and_scope_bindings(
|
|
|
922
1020
|
project_relative = test.get("project_path")
|
|
923
1021
|
inventory_relative = test.get("inventory_path")
|
|
924
1022
|
snapshot_relative = test.get("snapshot_path")
|
|
1023
|
+
change_status = test.get("change_status", "modified" if isinstance(snapshot_relative, str) else "existing")
|
|
1024
|
+
require(change_status in {"existing", "added", "modified"}, f"planned test has invalid change_status: {test_id}", errors)
|
|
925
1025
|
project_path = safe_path(project_root, project_relative, errors, f"planned test {test_id} project_path")
|
|
926
1026
|
require(project_path is not None and project_path.is_file(), f"planned test live path is missing: {test_id}", errors)
|
|
927
1027
|
require(inventory_relative == project_relative, f"planned test inventory path differs from live project path: {test_id}", errors)
|
|
928
1028
|
require(inventory_relative in inventory_by_category.get("tests", {}), f"planned test is not in the test inventory: {test_id}", errors)
|
|
929
|
-
|
|
930
|
-
|
|
1029
|
+
if change_status == "existing":
|
|
1030
|
+
require(snapshot_relative is None, f"existing test must not have a generated snapshot: {test_id}", errors)
|
|
1031
|
+
snapshot_path = None
|
|
1032
|
+
else:
|
|
1033
|
+
snapshot_path = require_manifest_entry(snapshot_relative, entry_map, root, errors, f"planned test snapshot {test_id}") if isinstance(snapshot_relative, str) else None
|
|
1034
|
+
require(isinstance(snapshot_relative, str) and path_under(snapshot_relative, "generated-tests"), f"planned test snapshot must be under generated-tests/: {test_id}", errors)
|
|
931
1035
|
if project_path is not None and project_path.is_file() and snapshot_path is not None and snapshot_path.is_file():
|
|
932
1036
|
require(file_sha256(project_path) == file_sha256(snapshot_path), f"generated test snapshot differs from live test: {test_id}", errors)
|
|
933
1037
|
test_layers = test.get("layers")
|
|
@@ -1082,7 +1186,7 @@ def validate_differential_proofs(
|
|
|
1082
1186
|
proof_by_behavior[behavior_id] = item
|
|
1083
1187
|
classification = item.get("classification")
|
|
1084
1188
|
if classification == "NOT_APPLICABLE":
|
|
1085
|
-
|
|
1189
|
+
validate_differential_na(item.get("not_applicable"), f"differential proof {behavior_id} not_applicable", errors)
|
|
1086
1190
|
for key in (
|
|
1087
1191
|
"method", "differential_request_path", "differential_request_sha256", "baseline_commit",
|
|
1088
1192
|
"current_tree_sha256", "baseline_execution_id", "mutation_execution_id", "current_execution_id",
|
|
@@ -1245,6 +1349,13 @@ def validate_verdict_and_attestation(
|
|
|
1245
1349
|
require(bool(verdict_record.get("blockers")), "TEST_BLOCKED lacks blocker reasons", errors)
|
|
1246
1350
|
elif verdict == "TEST_FAIL":
|
|
1247
1351
|
require(bool(verdict_record.get("reproduced_failures")), "TEST_FAIL lacks reproduced failures", errors)
|
|
1352
|
+
require(
|
|
1353
|
+
verdict_record.get("repair_scope") in {
|
|
1354
|
+
"test-infrastructure", "production", "runtime", "schema", "dependency", "public-interface",
|
|
1355
|
+
},
|
|
1356
|
+
"TEST_FAIL lacks a valid evidence-backed repair_scope",
|
|
1357
|
+
errors,
|
|
1358
|
+
)
|
|
1248
1359
|
|
|
1249
1360
|
if verify_attestation:
|
|
1250
1361
|
validate_definition(validation, "validation", records_schema, "validation", errors)
|
|
@@ -1264,6 +1375,10 @@ def validate_verdict_and_attestation(
|
|
|
1264
1375
|
require(validation.get("semantics") == "integrity-and-protocol-validation-not-hostile-producer-proof", "validation attestation semantics exceed integrity/protocol validation", errors)
|
|
1265
1376
|
require(validation.get("replay_receipt_ids") == replayed_ids, "validation replay receipt IDs do not match actual replay executions", errors)
|
|
1266
1377
|
|
|
1378
|
+
if root.joinpath("lifecycle.json").is_file():
|
|
1379
|
+
expected_report = render_report_text(root, manifest, verdict_record, validation)
|
|
1380
|
+
require(report == expected_report, "derived report does not match canonical rendering", errors)
|
|
1381
|
+
|
|
1267
1382
|
fragments = [
|
|
1268
1383
|
f"Verdict: {verdict}", f"Evidence ID: {manifest.get('evidence_id')}",
|
|
1269
1384
|
"Derived from structured evidence", "sensitivity=project-controlled",
|
|
@@ -1299,7 +1414,10 @@ def validate_evidence(
|
|
|
1299
1414
|
|
|
1300
1415
|
entry_map = validate_manifest(root, manifest, manifest_schema, records_schema, errors, ignored_hash_paths)
|
|
1301
1416
|
change_class = manifest.get("change_class")
|
|
1302
|
-
|
|
1417
|
+
final_verdict = manifest.get("final_verdict")
|
|
1418
|
+
required = COMMON_RECORDS
|
|
1419
|
+
if final_verdict != "TEST_BLOCKED":
|
|
1420
|
+
required = required | (BEHAVIOR_RECORDS if change_class == "behavior" else LIGHTWEIGHT_RECORDS)
|
|
1303
1421
|
for relative in required:
|
|
1304
1422
|
require(relative in entry_map, f"required evidence record not hashed: {relative}", errors)
|
|
1305
1423
|
validate_definition(classification, "changeClassification", records_schema, "change-classification", errors)
|
|
@@ -1322,7 +1440,10 @@ def validate_evidence(
|
|
|
1322
1440
|
|
|
1323
1441
|
inventory_by_category, _ = validate_inventory(root, project_root, manifest, inventory, scope, plan, entry_map, errors)
|
|
1324
1442
|
require(manifest.get("evidence_id_inputs", {}).get("scope_sha256") == canonical_sha256(scope), "scope identity hash mismatch", errors)
|
|
1325
|
-
validate_scope(
|
|
1443
|
+
validate_scope(
|
|
1444
|
+
scope, inventory, inventory_by_category, project_root, root,
|
|
1445
|
+
get_captured_deleted_paths(root), errors,
|
|
1446
|
+
)
|
|
1326
1447
|
execution_requests, differential_requests, executions = validate_requests_and_receipts(root, entry_map, records_schema, errors)
|
|
1327
1448
|
|
|
1328
1449
|
if change_class == "behavior":
|
|
@@ -1373,6 +1494,64 @@ def validate_evidence(
|
|
|
1373
1494
|
return errors
|
|
1374
1495
|
|
|
1375
1496
|
|
|
1497
|
+
def atomic_write_text(path: Path, value: str) -> None:
|
|
1498
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1499
|
+
descriptor, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent)
|
|
1500
|
+
try:
|
|
1501
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
|
1502
|
+
handle.write(value)
|
|
1503
|
+
os.replace(temporary_name, path)
|
|
1504
|
+
except BaseException:
|
|
1505
|
+
try:
|
|
1506
|
+
os.unlink(temporary_name)
|
|
1507
|
+
except FileNotFoundError:
|
|
1508
|
+
pass
|
|
1509
|
+
raise
|
|
1510
|
+
|
|
1511
|
+
|
|
1512
|
+
def render_report_text(
|
|
1513
|
+
root: Path,
|
|
1514
|
+
manifest: dict[str, Any],
|
|
1515
|
+
verdict: dict[str, Any],
|
|
1516
|
+
validation: dict[str, Any],
|
|
1517
|
+
) -> str:
|
|
1518
|
+
lifecycle_path = root / "lifecycle.json"
|
|
1519
|
+
lifecycle = load_json(lifecycle_path) if lifecycle_path.exists() else {}
|
|
1520
|
+
proof_path = root / "differential-proof.json"
|
|
1521
|
+
proof_record = load_json(proof_path) if proof_path.exists() else {}
|
|
1522
|
+
executions_record = load_json(root / "executions.json")
|
|
1523
|
+
proofs = proof_record.get("proofs", []) if isinstance(proof_record, dict) else []
|
|
1524
|
+
executions = executions_record if isinstance(executions_record, list) else []
|
|
1525
|
+
artifact_dir = lifecycle.get("artifact_dir") if isinstance(lifecycle, dict) else None
|
|
1526
|
+
lines = [
|
|
1527
|
+
"# Test Evidence Report",
|
|
1528
|
+
"",
|
|
1529
|
+
"Derived from structured evidence",
|
|
1530
|
+
f"Evidence ID: {manifest.get('evidence_id')}",
|
|
1531
|
+
f"Verdict: {verdict.get('verdict')}",
|
|
1532
|
+
f"Validator result: {validation.get('result', 'pending')}",
|
|
1533
|
+
"sensitivity=project-controlled",
|
|
1534
|
+
"The project owns access control, retention, and upload policy.",
|
|
1535
|
+
"Mocked code-level evidence does not verify a real deployed environment.",
|
|
1536
|
+
"",
|
|
1537
|
+
"## Scope",
|
|
1538
|
+
f"- Artifact Dir: {artifact_dir or 'not provided'}",
|
|
1539
|
+
"",
|
|
1540
|
+
f"Execution receipts: {len(executions)}",
|
|
1541
|
+
f"Differential proofs: {len(proofs) if isinstance(proofs, list) else 0}",
|
|
1542
|
+
"The legacy test-report interface is not supported.",
|
|
1543
|
+
"",
|
|
1544
|
+
]
|
|
1545
|
+
return "\n".join(lines)
|
|
1546
|
+
|
|
1547
|
+
|
|
1548
|
+
def refresh_final_record_hashes(root: Path, manifest: dict[str, Any]) -> None:
|
|
1549
|
+
for entry in manifest.get("files", []):
|
|
1550
|
+
if isinstance(entry, dict) and entry.get("path") in FINAL_RECORDS:
|
|
1551
|
+
entry["sha256"] = file_sha256(root / entry["path"])
|
|
1552
|
+
atomic_write_json(root / "manifest.json", manifest)
|
|
1553
|
+
|
|
1554
|
+
|
|
1376
1555
|
def main() -> int:
|
|
1377
1556
|
parser = argparse.ArgumentParser(description="Validate a PrizmKit test evidence package")
|
|
1378
1557
|
parser.add_argument("--evidence-dir", required=True, help="Path to .prizmkit/test/evidence/<evidence-id>")
|
|
@@ -1388,7 +1567,7 @@ def main() -> int:
|
|
|
1388
1567
|
except IndexError:
|
|
1389
1568
|
print("Test evidence validation: FAIL\n- cannot infer project root from evidence directory")
|
|
1390
1569
|
return 1
|
|
1391
|
-
errors = validate_evidence(root, project_root,
|
|
1570
|
+
errors = validate_evidence(root, project_root, ATTESTATION_MUTABLE if args.attest else None)
|
|
1392
1571
|
if errors:
|
|
1393
1572
|
print("Test evidence validation: FAIL")
|
|
1394
1573
|
for error in errors:
|
|
@@ -1396,6 +1575,7 @@ def main() -> int:
|
|
|
1396
1575
|
return 1
|
|
1397
1576
|
|
|
1398
1577
|
manifest = load_json(root / "manifest.json")
|
|
1578
|
+
verdict_record = load_json(root / "verdict.json")
|
|
1399
1579
|
if args.attest:
|
|
1400
1580
|
executions = load_json(root / "executions.json")
|
|
1401
1581
|
replayed_ids = [
|
|
@@ -1421,10 +1601,9 @@ def main() -> int:
|
|
|
1421
1601
|
"replay_receipt_ids": replayed_ids,
|
|
1422
1602
|
}
|
|
1423
1603
|
atomic_write_json(root / "validation.json", validation)
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
atomic_write_json(root / "manifest.json", manifest)
|
|
1604
|
+
report_text = render_report_text(root, manifest, verdict_record, validation)
|
|
1605
|
+
atomic_write_text(root / "test-report.md", report_text)
|
|
1606
|
+
refresh_final_record_hashes(root, manifest)
|
|
1428
1607
|
errors = validate_evidence(root, project_root)
|
|
1429
1608
|
if errors:
|
|
1430
1609
|
print("Test evidence validation: FAIL")
|
|
@@ -199,7 +199,7 @@ Interactive execution requires the committer to:
|
|
|
199
199
|
4. wait for explicit user confirmation from the current user;
|
|
200
200
|
5. create and verify the local commit only after confirmation.
|
|
201
201
|
|
|
202
|
-
Trusted headless execution is a separate atomic-stage path. It requires a host-defined non-interactive `mode`, a trusted `owner` identifier, `local_commit_authorized=true
|
|
202
|
+
Trusted headless execution is a separate atomic-stage path. It requires a host-defined non-interactive `mode`, a trusted `owner` identifier, and `local_commit_authorized=true`; it does not ask or wait. Unknown headless contexts are blocked. Remote publication is a separate host-runtime operation and is never decided by this composite workflow.
|
|
203
203
|
|
|
204
204
|
## Automatic Handoff and Manual Fallback
|
|
205
205
|
|
|
@@ -157,12 +157,12 @@ Trusted headless execution:
|
|
|
157
157
|
```text
|
|
158
158
|
a trusted host explicitly authorizes the current local task commit
|
|
159
159
|
→ host injects mode=<host-defined-headless-mode>, owner=<trusted-host-identifier>,
|
|
160
|
-
local_commit_authorized=true
|
|
160
|
+
local_commit_authorized=true
|
|
161
161
|
→ committer verifies gates and commits automatically
|
|
162
|
-
→ no question
|
|
162
|
+
→ no question or wait is permitted
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
-
A self-declared or otherwise untrusted headless context does not authorize a commit.
|
|
165
|
+
A self-declared or otherwise untrusted headless context does not authorize a commit. Remote publication remains a separate host-runtime decision.
|
|
166
166
|
|
|
167
167
|
## Recovery
|
|
168
168
|
|