refactor(validate): separate factual proof from semantic judgment

This commit is contained in:
boshu
2026-07-14 00:48:13 -04:00
parent 7499ed7045
commit 3569b98165
17 changed files with 400 additions and 34 deletions
@@ -28,10 +28,21 @@
"gate": {
"type": "object",
"additionalProperties": false,
"required": ["id", "lane", "argv", "backing"],
"required": ["id", "lane", "proof_kind", "argv", "backing"],
"properties": {
"id": { "type": "string", "minLength": 1, "pattern": "^[a-z0-9][a-z0-9._-]*$" },
"lane": { "enum": ["mandatory", "diagnostic", "release"] },
"proof_kind": {
"enum": [
"syntax",
"schema",
"identity",
"paths",
"generated_drift",
"executable_assertion",
"evidence_integrity"
]
},
"argv": {
"type": "array",
"minItems": 1,
+23 -2
View File
@@ -141,9 +141,19 @@
"preflightError": {
"type": "object",
"additionalProperties": false,
"required": ["code", "detail"],
"required": ["code", "defect_class", "detail"],
"properties": {
"code": { "type": "string", "minLength": 1 },
"defect_class": {
"enum": [
"candidate_integrity",
"evidence_integrity",
"registry_integrity",
"toolchain_integrity",
"validator_integrity",
"request_integrity"
]
},
"detail": { "type": "string", "minLength": 1 }
}
},
@@ -161,10 +171,21 @@
"gateExecution": {
"type": "object",
"additionalProperties": false,
"required": ["id", "lane", "candidate", "attribution"],
"required": ["id", "lane", "proof_kind", "candidate", "attribution"],
"properties": {
"id": { "type": "string", "minLength": 1 },
"lane": { "enum": ["mandatory", "diagnostic", "release"] },
"proof_kind": {
"enum": [
"syntax",
"schema",
"identity",
"paths",
"generated_drift",
"executable_assertion",
"evidence_integrity"
]
},
"candidate": { "$ref": "#/$defs/factualResult" },
"baseline": { "$ref": "#/$defs/factualResult" },
"attribution": { "enum": ["not_applicable", "candidate_introduced", "pre_existing"] }
+12 -1
View File
@@ -72,10 +72,21 @@
"selectedGate": {
"type": "object",
"additionalProperties": false,
"required": ["id", "lane", "entry_sha256"],
"required": ["id", "lane", "proof_kind", "entry_sha256"],
"properties": {
"id": { "type": "string", "minLength": 1 },
"lane": { "enum": ["mandatory", "diagnostic", "release"] },
"proof_kind": {
"enum": [
"syntax",
"schema",
"identity",
"paths",
"generated_drift",
"executable_assertion",
"evidence_integrity"
]
},
"entry_sha256": { "$ref": "#/$defs/digest" }
}
},
+8 -5
View File
@@ -243,15 +243,18 @@ add_step() {
}
if [[ "${#SOURCE_SKILLS[@]}" -gt 0 ]]; then
audit_cmd=""
integrity_cmd=""
for source_skill in "${SOURCE_SKILLS[@]}"; do
printf -v source_target '%q' "skills/$source_skill"
if [[ -n "$audit_cmd" ]]; then
audit_cmd+=" && "
if [[ -n "$integrity_cmd" ]]; then
integrity_cmd+=" && "
fi
audit_cmd+="bash skills/heal-skill/scripts/audit.sh --strict $source_target"
integrity_cmd+="bash skills/heal-skill/scripts/heal.sh --check --strict $source_target"
done
add_step "changed skill deep conformance|$audit_cmd|$audit_cmd"
# Changed-scope release proof owns structural package integrity only. Content
# quality and trigger usefulness are semantic evidence for the independent
# validator; they must not become a blocking keyword or prose-score gate.
add_step "changed skill structural integrity|$integrity_cmd|$integrity_cmd"
fi
if $NEED_CONTEXT_MAP; then
+2 -2
View File
@@ -951,8 +951,8 @@
{
"name": "validate",
"source_skill": "skills/validate",
"source_hash": "f9e93a7eba879fbc19dd23100524778a5239f9ee18ca1500c0821796e041ca83",
"generated_hash": "014ac9fd434556b30080d39ad570f371d062c431a4066ccb1a44b88c13abc388"
"source_hash": "37a96331e00095a5938604c80d705aaedd7796116c7c725efc2ea564c642656b",
"generated_hash": "6c4777ac971820b8ad164c28fdff6739d1a1f4a85ea76660d286e815de0ed812"
},
{
"name": "workflow-builder",
@@ -2,6 +2,6 @@
"generator": "codex-sync",
"source_skill": "skills/validate",
"layout": "modular",
"source_hash": "f9e93a7eba879fbc19dd23100524778a5239f9ee18ca1500c0821796e041ca83",
"generated_hash": "014ac9fd434556b30080d39ad570f371d062c431a4066ccb1a44b88c13abc388"
"source_hash": "37a96331e00095a5938604c80d705aaedd7796116c7c725efc2ea564c642656b",
"generated_hash": "6c4777ac971820b8ad164c28fdff6739d1a1f4a85ea76660d286e815de0ed812"
}
+7
View File
@@ -52,6 +52,13 @@ though it retains baseline attribution. Green mandatory proof routes to one
fresh validator by default. Inventory size is never a rigor or validator-count
signal.
Every factual registry entry declares one closed `proof_kind`: syntax, schema,
identity, paths, generated drift, executable assertion, or evidence integrity.
Semantic prose scores and exact-wording preferences are reviewer evidence, not
factual gate kinds. Missing backing is a typed `registry_integrity` defect;
neither that defect nor a semantic observation may be mislabeled as candidate
proof.
The portable freezer, runner, and receipt verifier is
`python3 skills/validate/scripts/validation-request.py freeze|run|check-receipt --help`.
@@ -119,6 +119,24 @@ skill count, changed-file count, or any other inventory count never selects
rigor or validator count. Risk and explicit mode selection are separate policy
inputs outside this request foundation.
### Deterministic and semantic boundary
Factual registry entries declare exactly one `proof_kind`: `syntax`, `schema`,
`identity`, `paths`, `generated_drift`, `executable_assertion`, or
`evidence_integrity`. The request and receipt retain that kind so consumers can
audit what the command was allowed to prove. A registry entry cannot declare
semantic prose quality, usefulness, preferred wording, or reviewer judgment as
machine proof.
Missing or stale gate registration, entry identity, or backing is a typed
`registry_integrity` defect and stops before execution. It is not evidence that
the candidate failed. Deterministic gates may still verify that a semantic
verdict is present, schema-valid, independently authored, and bound to the
candidate; only the independent reviewer decides whether its reasoning is
correct.
An advisory semantic observation never becomes deterministic authority. It
never blocks delivery by being promoted into a strict prose score.
The closed wire formats are:
- `schemas/validation-candidate.v1.schema.json`
@@ -19,3 +19,11 @@ Feature: Validate emits immutable proof only
Given the author and validator identities are equal
When the verdict would otherwise be PASS
Then independence is waived and the verdict cannot satisfy independent proof
Scenario: Factual proof does not impersonate semantic judgment
Given a frozen factual-gate registry and a pinned candidate
When deterministic pre-validation runs
Then each gate proves one declared factual proof kind
And missing backing is classified as registry integrity
And prose quality and exact wording remain independent-review evidence
And no advisory semantic observation becomes a blocking deterministic gate
@@ -351,6 +351,37 @@ def semantic_id(
return digest_bytes(canonical_bytes(identity))
def defect_class(code: str) -> str:
"""Name the owning integrity boundary for a fail-closed preflight defect."""
if code in {
"missing_gate_registry",
"invalid_gate_registry",
"unknown_gate",
"duplicate_gate",
"gate_not_factual_json",
"registry_entry_changed",
"missing_registry_backing",
"missing_mandatory_gate",
}:
return "registry_integrity"
if code in {"candidate_mutated", "base_not_ancestor"}:
return "candidate_integrity"
if code in {
"missing_acceptance",
"missing_claim_dependency",
"stale_claim_dependency",
"missing_evidence",
"claim_dependency_digest_mismatch",
"semantic_identity_changed",
}:
return "evidence_integrity"
if code == "missing_toolchain":
return "toolchain_integrity"
if code in {"validator_not_independent", "invalid_validator_route"}:
return "validator_integrity"
return "request_integrity"
def validate_registry(value: Any) -> dict[str, dict[str, Any]]:
validate_schema("validation-gate-registry.v1.schema.json", value)
by_id: dict[str, dict[str, Any]] = {}
@@ -455,6 +486,7 @@ def freeze(repo: Path, spec_path: Path, output: Path) -> int:
{
"id": gate_id,
"lane": entry["lane"],
"proof_kind": entry["proof_kind"],
"entry_sha256": digest_bytes(canonical_bytes(entry)),
}
)
@@ -649,7 +681,13 @@ def blocked_receipt(
"request_sha256": request_digest,
"candidate": request["candidate"],
"validator_route": request["validator"]["route"],
"preflight_errors": [{"code": error.code, "detail": error.detail}],
"preflight_errors": [
{
"code": error.code,
"defect_class": defect_class(error.code),
"detail": error.detail,
}
],
"gate_executions": [],
"model_spend_allowed": False,
"disposition": "BLOCK",
@@ -688,8 +726,13 @@ def validate_receipt_invariants(
)
return
expected_gates = [(item["id"], item["lane"]) for item in request["selected_gates"]]
actual_gates = [(item["id"], item["lane"]) for item in executions]
expected_gates = [
(item["id"], item["lane"], item["proof_kind"])
for item in request["selected_gates"]
]
actual_gates = [
(item["id"], item["lane"], item["proof_kind"]) for item in executions
]
if actual_gates != expected_gates:
raise RequestError(
"receipt_invariant", "gate executions do not exactly match selected gates"
@@ -811,6 +854,7 @@ def execute(repo: Path, request_path: Path, output: Path) -> int:
raise RequestError("unknown_gate", selected["id"])
if (
entry["lane"] != selected["lane"]
or entry["proof_kind"] != selected["proof_kind"]
or digest_bytes(canonical_bytes(entry)) != selected["entry_sha256"]
):
raise RequestError("registry_entry_changed", selected["id"])
@@ -886,6 +930,7 @@ def execute(repo: Path, request_path: Path, output: Path) -> int:
execution: dict[str, Any] = {
"id": selected["id"],
"lane": selected["lane"],
"proof_kind": selected["proof_kind"],
"candidate": candidate_result,
"attribution": "not_applicable",
}
+7
View File
@@ -77,6 +77,13 @@ though it retains baseline attribution. Green mandatory proof routes to one
fresh validator by default. Inventory size is never a rigor or validator-count
signal.
Every factual registry entry declares one closed `proof_kind`: syntax, schema,
identity, paths, generated drift, executable assertion, or evidence integrity.
Semantic prose scores and exact-wording preferences are reviewer evidence, not
factual gate kinds. Missing backing is a typed `registry_integrity` defect;
neither that defect nor a semantic observation may be mislabeled as candidate
proof.
The portable freezer, runner, and receipt verifier is
`python3 skills/validate/scripts/validation-request.py freeze|run|check-receipt --help`.
@@ -119,6 +119,24 @@ skill count, changed-file count, or any other inventory count never selects
rigor or validator count. Risk and explicit mode selection are separate policy
inputs outside this request foundation.
### Deterministic and semantic boundary
Factual registry entries declare exactly one `proof_kind`: `syntax`, `schema`,
`identity`, `paths`, `generated_drift`, `executable_assertion`, or
`evidence_integrity`. The request and receipt retain that kind so consumers can
audit what the command was allowed to prove. A registry entry cannot declare
semantic prose quality, usefulness, preferred wording, or reviewer judgment as
machine proof.
Missing or stale gate registration, entry identity, or backing is a typed
`registry_integrity` defect and stops before execution. It is not evidence that
the candidate failed. Deterministic gates may still verify that a semantic
verdict is present, schema-valid, independently authored, and bound to the
candidate; only the independent reviewer decides whether its reasoning is
correct.
An advisory semantic observation never becomes deterministic authority. It
never blocks delivery by being promoted into a strict prose score.
The closed wire formats are:
- `schemas/validation-candidate.v1.schema.json`
@@ -19,3 +19,11 @@ Feature: Validate emits immutable proof only
Given the author and validator identities are equal
When the verdict would otherwise be PASS
Then independence is waived and the verdict cannot satisfy independent proof
Scenario: Factual proof does not impersonate semantic judgment
Given a frozen factual-gate registry and a pinned candidate
When deterministic pre-validation runs
Then each gate proves one declared factual proof kind
And missing backing is classified as registry integrity
And prose quality and exact wording remain independent-review evidence
And no advisory semantic observation becomes a blocking deterministic gate
+48 -3
View File
@@ -351,6 +351,37 @@ def semantic_id(
return digest_bytes(canonical_bytes(identity))
def defect_class(code: str) -> str:
"""Name the owning integrity boundary for a fail-closed preflight defect."""
if code in {
"missing_gate_registry",
"invalid_gate_registry",
"unknown_gate",
"duplicate_gate",
"gate_not_factual_json",
"registry_entry_changed",
"missing_registry_backing",
"missing_mandatory_gate",
}:
return "registry_integrity"
if code in {"candidate_mutated", "base_not_ancestor"}:
return "candidate_integrity"
if code in {
"missing_acceptance",
"missing_claim_dependency",
"stale_claim_dependency",
"missing_evidence",
"claim_dependency_digest_mismatch",
"semantic_identity_changed",
}:
return "evidence_integrity"
if code == "missing_toolchain":
return "toolchain_integrity"
if code in {"validator_not_independent", "invalid_validator_route"}:
return "validator_integrity"
return "request_integrity"
def validate_registry(value: Any) -> dict[str, dict[str, Any]]:
validate_schema("validation-gate-registry.v1.schema.json", value)
by_id: dict[str, dict[str, Any]] = {}
@@ -455,6 +486,7 @@ def freeze(repo: Path, spec_path: Path, output: Path) -> int:
{
"id": gate_id,
"lane": entry["lane"],
"proof_kind": entry["proof_kind"],
"entry_sha256": digest_bytes(canonical_bytes(entry)),
}
)
@@ -649,7 +681,13 @@ def blocked_receipt(
"request_sha256": request_digest,
"candidate": request["candidate"],
"validator_route": request["validator"]["route"],
"preflight_errors": [{"code": error.code, "detail": error.detail}],
"preflight_errors": [
{
"code": error.code,
"defect_class": defect_class(error.code),
"detail": error.detail,
}
],
"gate_executions": [],
"model_spend_allowed": False,
"disposition": "BLOCK",
@@ -688,8 +726,13 @@ def validate_receipt_invariants(
)
return
expected_gates = [(item["id"], item["lane"]) for item in request["selected_gates"]]
actual_gates = [(item["id"], item["lane"]) for item in executions]
expected_gates = [
(item["id"], item["lane"], item["proof_kind"])
for item in request["selected_gates"]
]
actual_gates = [
(item["id"], item["lane"], item["proof_kind"]) for item in executions
]
if actual_gates != expected_gates:
raise RequestError(
"receipt_invariant", "gate executions do not exactly match selected gates"
@@ -811,6 +854,7 @@ def execute(repo: Path, request_path: Path, output: Path) -> int:
raise RequestError("unknown_gate", selected["id"])
if (
entry["lane"] != selected["lane"]
or entry["proof_kind"] != selected["proof_kind"]
or digest_bytes(canonical_bytes(entry)) != selected["entry_sha256"]
):
raise RequestError("registry_entry_changed", selected["id"])
@@ -886,6 +930,7 @@ def execute(repo: Path, request_path: Path, output: Path) -> int:
execution: dict[str, Any] = {
"id": selected["id"],
"lane": selected["lane"],
"proof_kind": selected["proof_kind"],
"candidate": candidate_result,
"attribution": "not_applicable",
}
@@ -0,0 +1,120 @@
#!/usr/bin/env bats
setup() {
REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)"
}
@test "factual registry admits only declared deterministic proof kinds" {
run env REPO_ROOT="$REPO_ROOT" python3 - <<'PY'
import json
import os
from pathlib import Path
from jsonschema import Draft202012Validator
root = Path(os.environ["REPO_ROOT"])
schema = json.loads(
(root / "schemas/validation-gate-registry.v1.schema.json").read_text()
)
validator = Draft202012Validator(schema)
allowed = {
"syntax",
"schema",
"identity",
"paths",
"generated_drift",
"executable_assertion",
"evidence_integrity",
}
for proof_kind in sorted(allowed):
registry = {
"schema_version": 1,
"gates": [{
"id": "fact",
"lane": "mandatory",
"proof_kind": proof_kind,
"argv": ["bash", "scripts/fact.sh", "--json"],
"backing": [{"path": "scripts/fact.sh", "sha256": "0" * 64}],
}],
}
errors = list(validator.iter_errors(registry))
if errors:
raise SystemExit(f"allowed proof kind {proof_kind!r} was rejected: {errors[0].message}")
semantic = {
"schema_version": 1,
"gates": [{
"id": "semantic-prose-score",
"lane": "mandatory",
"proof_kind": "semantic_prose",
"argv": ["bash", "scripts/score-prose.sh", "--json"],
"backing": [{"path": "scripts/score-prose.sh", "sha256": "0" * 64}],
}],
}
if not list(validator.iter_errors(semantic)):
raise SystemExit("semantic prose was accepted as deterministic proof")
PY
[ "$status" -eq 0 ]
}
@test "missing registry backing has an explicit registry-integrity class" {
run env REPO_ROOT="$REPO_ROOT" python3 - <<'PY'
import json
import os
from pathlib import Path
schema = json.loads(
(Path(os.environ["REPO_ROOT"]) / "schemas/validation-receipt.v1.schema.json").read_text()
)
error = schema["$defs"]["preflightError"]
required = set(error["required"])
if "defect_class" not in required:
raise SystemExit("preflight errors do not require a defect class")
enum = set(error["properties"]["defect_class"]["enum"])
if "registry_integrity" not in enum:
raise SystemExit("registry_integrity is not a declared defect class")
PY
[ "$status" -eq 0 ]
}
@test "changed-scope gate runs structural integrity without semantic scoring" {
run bash "$REPO_ROOT/scripts/regen-changed-scope.sh" --list \
--file skills/validate/SKILL.md
[ "$status" -eq 0 ]
[[ "$output" == *"changed skill structural integrity"* ]]
[[ "$output" == *"skills/heal-skill/scripts/heal.sh --check --strict skills/validate"* ]]
[[ "$output" != *"skills/heal-skill/scripts/audit.sh"* ]]
[[ "$output" != *"deep conformance"* ]]
}
@test "equivalent prose rephrasing cannot fail deterministic Validate proof" {
local scratch="$BATS_TEST_TMPDIR/rephrased-validate"
mkdir -p "$scratch/skills" "$scratch/schemas"
cp -R "$REPO_ROOT/skills/validate" "$scratch/skills/validate"
cp "$REPO_ROOT/schemas/verdict.v1.schema.json" \
"$REPO_ROOT"/schemas/validation-*.schema.json "$scratch/schemas/"
for document in \
"$scratch/skills/validate/SKILL.md" \
"$scratch/skills/validate/references/canonical-validation-protocol.md"; do
awk '
!/Semantic prose scores/ &&
!/exact-wording preferences/ &&
!/advisory semantic observation/ &&
!/deterministic authority/
' "$document" >"$document.rephrased"
mv "$document.rephrased" "$document"
done
printf '\nEquivalent wording: machines establish facts; independent reviewers judge meaning.\n' \
>>"$scratch/skills/validate/SKILL.md"
printf '\nEquivalent wording: advisory comments about meaning cannot authorize or block delivery.\n' \
>>"$scratch/skills/validate/references/canonical-validation-protocol.md"
run bash "$scratch/skills/validate/scripts/validate.sh"
[ "$status" -eq 0 ]
[[ "$output" == *"validate skill contract: PASS"* ]]
}
+26 -12
View File
@@ -870,7 +870,7 @@ PY
[[ "$output" == *'repo-runtime'* ]]
}
@test "L2: changed-scope rejects shallow-green evidence when deep audit is non-PASS" {
@test "L2: changed-scope keeps semantic skill scoring off the deterministic gate" {
local scratch="$BATS_TEST_TMPDIR/release-waist-root"
local scan_root="$BATS_TEST_TMPDIR/release-waist-scan"
local shallow_bats="$BATS_TEST_TMPDIR/shallow-count.bats"
@@ -879,6 +879,20 @@ PY
prepare_builder_root "$scratch"
write_conforming_skill "$scratch/skills/shallow-green" shallow-green \
"'Shallow checks pass. Triggers: \"shallow green\".'" incomplete
cat >>"$scratch/docs/contracts/skill-dispositions.yaml" <<'YAML'
- skill: shallow-green
domain: "BC3 Loop"
hexagonal_role: supporting
disposition: keep
kind: skill
runtime_targets: [claude, codex]
parity_policy: required
capability_class: execution
path: skills/shallow-green/SKILL.md
aliases: []
supersedes: null
rationale: "Fixture proves semantic scoring stays out of deterministic changed-scope checks."
YAML
mkdir -p "$scan_root"
cp -R "$scratch/skills/shallow-green" "$scan_root/shallow-green"
@@ -903,13 +917,13 @@ BATS
run bash "$scratch/scripts/regen-changed-scope.sh" --check \
--file skills/shallow-green/SKILL.md
[[ "$status" -ne 0 ]]
[[ "$output" == *'changed skill deep conformance'* ]]
[[ "$output" == *'output-spec-explicit'* ]]
[[ "$output" == *'bash skills/heal-skill/scripts/audit.sh --strict skills/shallow-green'* ]]
[[ "$status" -eq 0 ]]
[[ "$output" == *'changed skill structural integrity'* ]]
[[ "$output" == *'All clean. No findings.'* ]]
[[ "$output" != *'output-spec-explicit'* ]]
}
@test "L2: changed-scope does not deep-audit redirect-only runtime packages" {
@test "L2: changed-scope does not inspect redirect-only runtime packages" {
local scratch="$BATS_TEST_TMPDIR/redirect-scope-root"
prepare_builder_root "$scratch"
@@ -917,15 +931,15 @@ BATS
run bash "$scratch/scripts/regen-changed-scope.sh" --list \
--file skills/pre-mortem/SKILL.md
[[ "$status" -eq 0 ]]
[[ "$output" != *'changed skill deep conformance'* ]]
[[ "$output" != *'audit.sh --strict skills/pre-mortem'* ]]
[[ "$output" != *'changed skill structural integrity'* ]]
[[ "$output" != *'heal.sh --check --strict skills/pre-mortem'* ]]
[[ "$output" == *'codex'* ]]
[[ "$output" == *'registry'* ]]
}
@test "L0: local and CI release waists invoke the canonical deep audit" {
grep -Fq 'skills/heal-skill/scripts/audit.sh --strict' \
@test "L0: changed-scope release waist invokes structural integrity, not prose scoring" {
grep -Fq 'skills/heal-skill/scripts/heal.sh --check --strict' \
"$REPO_ROOT/scripts/regen-changed-scope.sh"
! grep -Fq 'skills/heal-skill/scripts/audit.sh --strict' \
"$REPO_ROOT/scripts/regen-changed-scope.sh"
grep -Fq 'skills/heal-skill/scripts/audit.sh --strict' \
"$REPO_ROOT/cli/internal/gates/checks/seed.go"
}
+33 -3
View File
@@ -79,6 +79,7 @@ GATE
gates: [{
id: "fact",
lane: "mandatory",
proof_kind: "executable_assertion",
argv: ["bash", "scripts/factual-gate.sh", $count, "--json"],
backing: [{path: $backing_path, sha256: $backing_sha}]
}]
@@ -153,6 +154,7 @@ add_nonbinding_gate() {
.gates += [{
id: $gate_id,
lane: $lane,
proof_kind: "executable_assertion",
argv: ["bash", "scripts/factual-gate.sh", $count, $status_file, "--json"],
backing: [{path: "scripts/factual-gate.sh", sha256: $backing_sha}]
}]
@@ -182,6 +184,7 @@ check_bad_receipt() {
(.candidate.semantic_id | length) == 64 and
(.candidate.changed_surfaces | length) >= 1 and
.validator.route == "single_fresh" and
.selected_gates[0].proof_kind == "executable_assertion" and
(.validator | has("inventory_count") | not)
' "$REQUEST"
@@ -192,6 +195,7 @@ check_bad_receipt() {
.disposition == "READY" and
.next_action == "VALIDATE_SINGLE_FRESH" and
(.gate_executions | length) == 1 and
.gate_executions[0].proof_kind == "executable_assertion" and
.gate_executions[0].candidate.status == "PASS"
' "$RECEIPT"
[ "$(wc -l <"$COUNT_FILE" | tr -d ' ')" -eq 1 ]
@@ -214,6 +218,7 @@ check_bad_receipt() {
.gates += [{
id: "fact-two",
lane: "mandatory",
proof_kind: "schema",
argv: ["bash", "scripts/factual-gate.sh", $count, "--json"],
backing: [{path: "scripts/factual-gate.sh", sha256: $backing_sha}]
}]
@@ -231,7 +236,10 @@ check_bad_receipt() {
run_request
[ "$status" -eq 0 ]
jq -e '.gate_executions | map(.id) == ["fact", "fact-two"]' "$RECEIPT"
jq -e '
(.gate_executions | map(.id)) == ["fact", "fact-two"] and
(.gate_executions | map(.proof_kind)) == ["executable_assertion", "schema"]
' "$RECEIPT"
[ "$(wc -l <"$COUNT_FILE" | tr -d ' ')" -eq 2 ]
}
@@ -283,7 +291,11 @@ check_bad_receipt() {
run_request
[ "$status" -eq 1 ]
jq -e '.model_spend_allowed == false and .preflight_errors[0].code == "missing_registry_backing"' "$RECEIPT"
jq -e '
.model_spend_allowed == false and
.preflight_errors[0].code == "missing_registry_backing" and
.preflight_errors[0].defect_class == "registry_integrity"
' "$RECEIPT"
[ ! -s "$COUNT_FILE" ]
}
@@ -686,13 +698,31 @@ check_bad_receipt() {
[ "$status" -eq 1 ]
}
@test "receipt rejects a proof kind that differs from the frozen registry" {
seed_repo PASS PASS
freeze_request
[ "$status" -eq 0 ]
run_request
[ "$status" -eq 0 ]
jq '.gate_executions[0].proof_kind = "schema"' "$RECEIPT" >"$BAD_RECEIPT"
check_bad_receipt
[ "$status" -eq 1 ]
[[ "$output" == *"gate executions do not exactly match selected gates"* ]]
}
@test "receipt rejects contradictory READY preflight evidence" {
seed_repo PASS PASS
freeze_request
[ "$status" -eq 0 ]
run_request
[ "$status" -eq 0 ]
jq '.preflight_errors = [{code: "stale_claim_dependency", detail: "claim.txt"}]' \
jq '.preflight_errors = [{
code: "stale_claim_dependency",
defect_class: "evidence_integrity",
detail: "claim.txt"
}]' \
"$RECEIPT" >"$BAD_RECEIPT"
check_bad_receipt