From efbb3b735e9dc7e558181f1a877f48e325ccb400 Mon Sep 17 00:00:00 2001 From: ci Date: Sat, 29 Aug 2026 21:19:42 -0400 Subject: [PATCH 1/6] core: add portable schema generation --- ci/required-files.txt | 7 + core/v1/generation-registry.json | 1 + .../modules/schema.jq | 367 ++++++++++++ .../test/portable-core-schema-fixtures.json | 238 ++++++++ scripts/test/portable-core-schema-ledger.tsv | 53 ++ scripts/test/portable-core-schema.test.sh | 551 ++++++++++++++++++ 6 files changed, 1217 insertions(+) create mode 100644 core/v1/generation-registry.json create mode 100644 core/v1/generations/g-14b7ad8ce54c3b8c585ff92063d71551ffc7394cc2294d0297bc7d2b8da2c386/modules/schema.jq create mode 100644 scripts/test/portable-core-schema-fixtures.json create mode 100644 scripts/test/portable-core-schema-ledger.tsv create mode 100755 scripts/test/portable-core-schema.test.sh diff --git a/ci/required-files.txt b/ci/required-files.txt index 890edf8..9bb46d9 100644 --- a/ci/required-files.txt +++ b/ci/required-files.txt @@ -87,3 +87,10 @@ scripts/test/north-star-gate.test.sh scripts/test/models-conf-parser.test.sh scripts/test/codex-degraded-gate.test.sh scripts/test/run-all.sh + +# Inactive portable core generation under construction +core/v1/generation-registry.json +core/v1/generations/g-14b7ad8ce54c3b8c585ff92063d71551ffc7394cc2294d0297bc7d2b8da2c386/modules/schema.jq +scripts/test/portable-core-schema-fixtures.json +scripts/test/portable-core-schema-ledger.tsv +scripts/test/portable-core-schema.test.sh diff --git a/core/v1/generation-registry.json b/core/v1/generation-registry.json new file mode 100644 index 0000000..5e11310 --- /dev/null +++ b/core/v1/generation-registry.json @@ -0,0 +1 @@ +[{"generation_id":"g-14b7ad8ce54c3b8c585ff92063d71551ffc7394cc2294d0297bc7d2b8da2c386","parent_plan_merge_commit":"38a26f5f046897c0455fef24874c5dbb40c20926","parent_spec_blob":"c6511d96c1a5e6aed27ba2075b5add65c121f782"}] diff --git a/core/v1/generations/g-14b7ad8ce54c3b8c585ff92063d71551ffc7394cc2294d0297bc7d2b8da2c386/modules/schema.jq b/core/v1/generations/g-14b7ad8ce54c3b8c585ff92063d71551ffc7394cc2294d0297bc7d2b8da2c386/modules/schema.jq new file mode 100644 index 0000000..a04596e --- /dev/null +++ b/core/v1/generations/g-14b7ad8ce54c3b8c585ff92063d71551ffc7394cc2294d0297bc7d2b8da2c386/modules/schema.jq @@ -0,0 +1,367 @@ +def policy_table: + { + roles: [ + {id:"ci", class:"dormant", execution_kinds:["deterministic"], capabilities:[]}, + {id:"execution", class:"dormant", execution_kinds:["deterministic"], capabilities:[]}, + {id:"forge", class:"dormant", execution_kinds:["deterministic"], capabilities:[]}, + {id:"identity", class:"dormant", execution_kinds:["deterministic"], capabilities:[]}, + {id:"producer", class:"protected", execution_kinds:["deterministic","model"], capabilities:["core.harness.produce.v1"]}, + {id:"publisher", class:"protected-dormant", execution_kinds:["deterministic"], capabilities:[]}, + {id:"reviewer", class:"protected", execution_kinds:["deterministic","model"], capabilities:["core.review.change.v1"]}, + {id:"verifier", class:"protected", execution_kinds:["deterministic"], capabilities:["core.verify.run.v1"]} + ], + actor_only_roles: ["manager","observer","operator","orchestrator"], + capabilities: [ + { + id:"core.harness.produce.v1", + role:"producer", + argument_shape:"produce", + outcome_family:"change", + permissions_by_execution:{ + deterministic:["core.perm.evidence.write.v1","core.perm.scratch.write.v1","core.perm.target.read.v1"], + model:["core.perm.evidence.write.v1","core.perm.model.invoke.v1","core.perm.scratch.write.v1","core.perm.target.read.v1"] + }, + allowed_evidence:["deterministic"], + required_evidence:["deterministic"] + }, + { + id:"core.review.change.v1", + role:"reviewer", + argument_shape:"review", + outcome_family:"check", + permissions_by_execution:{ + deterministic:["core.perm.evidence.write.v1","core.perm.target.read.v1"], + model:["core.perm.evidence.write.v1","core.perm.model.invoke.v1","core.perm.target.read.v1"] + }, + allowed_evidence:["independent-review"], + required_evidence:["independent-review"] + }, + { + id:"core.verify.run.v1", + role:"verifier", + argument_shape:"verify", + outcome_family:"check", + permissions_by_execution:{ + deterministic:["core.perm.candidate.execute.v1","core.perm.evidence.write.v1","core.perm.target.read.v1"] + }, + allowed_evidence:["architecture","behavioral","deterministic"], + required_evidence:["deterministic"] + } + ], + permissions: [ + {id:"core.perm.candidate.execute.v1",resource:"exact-candidate",actions:["execute"]}, + {id:"core.perm.evidence.write.v1",resource:"current-attempt-evidence",actions:["append"]}, + {id:"core.perm.model.invoke.v1",resource:"exact-resolved-model-binding",actions:["invoke"]}, + {id:"core.perm.scratch.write.v1",resource:"caller-disposable-scratch",actions:["write"]}, + {id:"core.perm.target.read.v1",resource:"exact-target-git",actions:["read"]} + ], + evidence: [ + {id:"architecture",verdicts:["failed","inconclusive","passed"]}, + {id:"behavioral",verdicts:["failed","inconclusive","passed"]}, + {id:"deterministic",verdicts:["failed","inconclusive","passed"]}, + {id:"independent-review",verdicts:["failed","inconclusive","passed"]} + ] + }; + +def document_kinds: + ["adapter_manifest","profile","resolved_profile","stage_request","stage_result"]; + +def semantic_identity: "core.contracts.v1"; +def adapter_roles: policy_table.roles | map(.id); +def actor_roles: (adapter_roles + policy_table.actor_only_roles) | sort; +def protected_roles: + policy_table.roles | + map(select(.class == "protected" or .class == "protected-dormant") | .id); +def capability_ids: policy_table.capabilities | map(.id); +def permission_ids: policy_table.permissions | map(.id); +def evidence_kinds: policy_table.evidence | map(.id); +def execution_kinds: ["deterministic","model"]; +def scope_purposes: + ["allowed-delta","authority","config-contract","finish-condition","gate-decision", + "gate-requirement","grant","output-contract","policy","qualification", + "repository-context","review-policy","selection","verification-instructions", + "verification-plan"]; + +def capabilities_for_role($role): + [policy_table.roles[] | select(.id == $role) | .capabilities] | + if length == 1 then .[0] else [] end; +def execution_kinds_for_role($role): + [policy_table.roles[] | select(.id == $role) | .execution_kinds] | + if length == 1 then .[0] else [] end; +def capability_execution_kinds($capability): + [policy_table.capabilities[] | + select(.id == $capability) | + (.permissions_by_execution | keys)] | + if length == 1 then .[0] else [] end; +def permissions_for_capability($capability; $execution_kind): + [policy_table.capabilities[] | + select(.id == $capability) | + .permissions_by_execution[$execution_kind] | + select(. != null)] | + if length == 1 then .[0] else [] end; +def allowed_evidence_kinds_for_capability($capability): + [policy_table.capabilities[] | select(.id == $capability) | .allowed_evidence] | + if length == 1 then .[0] else [] end; +def required_evidence_kinds_for_capability($capability): + [policy_table.capabilities[] | select(.id == $capability) | .required_evidence] | + if length == 1 then .[0] else [] end; +def evidence_verdicts: + policy_table.evidence | map(.verdicts) | unique | + if length == 1 then .[0] else [] end; + +def exact_fields($required; $optional): + . as $value | + ($value | type) == "object" and + (($value | keys_unsorted) - ($required + $optional) | length) == 0 and + all($required[]; . as $key | $value | has($key)); + +def bounded_set($minimum; $maximum; item_ok; key): + . as $items | + ($items | type) == "array" and + ($items | length) >= $minimum and + ($items | length) <= $maximum and + all($items[]; item_ok) and + (($items | map(key)) as $keys | + ($keys | length) == ($keys | unique | length) and + $keys == ($keys | sort)); + +def enum_set_ok($minimum; $maximum; $allowed): + . as $items | + ($items | type) == "array" and + ($items | length) >= $minimum and + ($items | length) <= $maximum and + all($items[]; . as $item | $allowed | index($item) != null) and + ($items | length) == ($items | unique | length) and + $items == ($items | sort); + +def present_ok(value_ok): + (exact_fields(["state"];[]) and .state == "absent") or + (exact_fields(["state","value"];[]) and + .state == "present" and + (.value | value_ok)); + +def id_ok: type == "string" and test("^[a-z0-9][a-z0-9._:-]{0,127}$"); +def int_ok: type == "number" and . == floor and . >= 0 and . <= 2147483647; +def sha256_ok: type == "string" and test("^[0-9a-f]{64}$"); +def version_ok: id_ok; +def short_text_ok: type == "string" and utf8bytelength >= 1 and utf8bytelength <= 1024; +def media_type_ok: + type == "string" and + length <= 127 and + test("^[a-z0-9][a-z0-9!#$&^_.+-]*/[a-z0-9][a-z0-9!#$&^_.+-]*$"); +def patch_media_type_ok: . == "text/x-diff"; +def git_oid_ok: type == "string" and (test("^[0-9a-f]{40}$") or test("^[0-9a-f]{64}$")); +def reverse_dns_ok: + type == "string" and + (split(".") as $labels | + ($labels | length) >= 2 and + all($labels[]; test("^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$"))); + +def time_ok: + type == "string" and + test("^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$") and + (capture("^(?[0-9]{4})-(?[0-9]{2})-(?[0-9]{2})T(?[0-9]{2}):(?[0-9]{2}):(?[0-9]{2})Z$") as $parts | + ($parts.year | tonumber) as $year | + ($parts.month | tonumber) as $month | + ($parts.day | tonumber) as $day | + ($parts.hour | tonumber) as $hour | + ($parts.minute | tonumber) as $minute | + ($parts.second | tonumber) as $second | + ($year % 4 == 0 and ($year % 100 != 0 or $year % 400 == 0)) as $leap | + [31,(if $leap then 29 else 28 end),31,30,31,30,31,31,30,31,30,31] as $days | + $month >= 1 and $month <= 12 and + $day >= 1 and $day <= $days[$month - 1] and + $hour >= 0 and $hour <= 23 and + $minute >= 0 and $minute <= 59 and + $second >= 0 and $second <= 59); + +# Oniguruma's bare byte escapes do not match multibyte C1 code points in UTF-8 mode. +def repo_path_ok: + type == "string" and + length > 0 and + (test("[\\x{0000}-\\x{001f}\\x{007f}-\\x{009f}]") | not) and + (contains("\\") | not) and + (split("/") | all(.[]; . != "" and . != "." and . != "..")); + +def parsed_limits_ok: + def within($depth): + if $depth > 32 then false + elif type == "object" then + length <= 256 and + all(keys_unsorted[]; utf8bytelength <= 8192) and + all(.[]; within($depth + 1)) + elif type == "array" then + length <= 256 and all(.[]; within($depth + 1)) + elif type == "string" then utf8bytelength <= 8192 + elif type == "number" then int_ok + else true + end; + within(0); + +def document_kind_ok: + type == "string" and (. as $kind | document_kinds | index($kind) != null); +def adapter_role_ok: + type == "string" and (. as $role | adapter_roles | index($role) != null); +def actor_role_ok: + type == "string" and (. as $role | actor_roles | index($role) != null); +def capability_id_ok: + type == "string" and (. as $id | capability_ids | index($id) != null); +def permission_id_ok: + type == "string" and (. as $id | permission_ids | index($id) != null); +def evidence_kind_ok: + type == "string" and (. as $kind | evidence_kinds | index($kind) != null); +def execution_kind_ok: + type == "string" and (. as $kind | execution_kinds | index($kind) != null); +def scope_purpose_ok: + type == "string" and (. as $purpose | scope_purposes | index($purpose) != null); + +def envelope_ok($kind): + exact_fields(["schema_version","kind","id","body"];[]) and + .schema_version == 1 and + .kind == $kind and + (.kind | document_kind_ok) and + (.id | id_ok) and + (.body | type == "object"); + +def document_envelope_ok: + (.kind? // "") as $kind | + ($kind | document_kind_ok) and envelope_ok($kind); + +def schema_layer_ok: parsed_limits_ok and document_envelope_ok; + +def document_ref_ok: + exact_fields(["schema_version","kind","id","sha256"];[]) and + .schema_version == 1 and + (.kind | document_kind_ok) and + (.id | id_ok) and + (.sha256 | sha256_ok); +def document_ref_kind_ok($kind): document_ref_ok and .kind == $kind; + +def git_revision_ref_ok: + exact_fields(["repository_id","hash_algorithm","commit_id"];[]) and + (.repository_id | id_ok) and + (.hash_algorithm == "sha1" or .hash_algorithm == "sha256") and + (if .hash_algorithm == "sha1" + then (.commit_id | type == "string" and test("^[0-9a-f]{40}$")) + else (.commit_id | type == "string" and test("^[0-9a-f]{64}$")) + end); + +def git_location_ok: + (exact_fields(["kind"];[]) and .kind == "root") or + (exact_fields(["kind","value"];[]) and + .kind == "path" and + (.value | repo_path_ok)); + +def git_object_ref_ok: + exact_fields(["revision","location","object_type","object_id","mode"];[]) and + (.revision | git_revision_ref_ok) and + (.location | git_location_ok) and + (.object_type == "blob" or .object_type == "tree") and + (if .revision.hash_algorithm == "sha1" + then (.object_id | type == "string" and test("^[0-9a-f]{40}$")) + else (.object_id | type == "string" and test("^[0-9a-f]{64}$")) + end) and + (if .location.kind == "root" then .object_type == "tree" else true end) and + (if .object_type == "tree" + then .mode == "040000" + else (.mode == "100644" or .mode == "100755") + end); + +def content_ref_ok: + exact_fields(["content_id","media_type","sha256"];[]) and + (.content_id | id_ok) and + (.media_type | media_type_ok) and + (.sha256 | sha256_ok); + +def artifact_ref_ok: + exact_fields(["type","value"];[]) and + ((.type == "git-object" and (.value | git_object_ref_ok)) or + (.type == "content" and (.value | content_ref_ok))); + +def input_ref_ok: + exact_fields(["type","value"];[]) and + ((.type == "artifact" and (.value | artifact_ref_ok)) or + (.type == "document" and (.value | document_ref_ok))); + +def evidence_ref_ok: + exact_fields(["stage_result_ref","evidence_id"];[]) and + (.stage_result_ref | document_ref_kind_ok("stage_result")) and + (.evidence_id | id_ok); + +def scope_subject_ok: + exact_fields(["type","value"];[]) and + ((.type == "artifact" and (.value | artifact_ref_ok)) or + (.type == "document" and (.value | document_ref_ok))); + +def scope_ref_ok: + exact_fields(["purpose","decision_record_ref","subject_ref","scope_sha256"];[]) and + (.purpose | scope_purpose_ok) and + (.decision_record_ref | content_ref_ok) and + (.subject_ref | scope_subject_ok) and + (.scope_sha256 | sha256_ok); +def scope_ref_purpose_ok($purpose): scope_ref_ok and .purpose == $purpose; + +def actor_ref_ok: + exact_fields( + ["role","implementation_id","implementation_version","adapter_instance_id", + "principal_id","execution_boundary_id"]; + ["authority_ref"]) and + (.role | actor_role_ok) and + (.implementation_id | id_ok) and + (.implementation_version | version_ok) and + (.adapter_instance_id | id_ok) and + (.principal_id | id_ok) and + (.execution_boundary_id | id_ok) and + ((has("authority_ref") | not) or (.authority_ref | scope_ref_purpose_ok("authority"))); + +def environment_ref_ok: + exact_fields(["environment_id","fingerprint_sha256"];[]) and + (.environment_id | id_ok) and + (.fingerprint_sha256 | sha256_ok); + +def tool_ref_ok: + exact_fields(["tool_id","tool_version","package_ref","config_ref"];[]) and + (.tool_id | id_ok) and + (.tool_version | version_ok) and + (.package_ref | git_object_ref_ok) and + (.config_ref | present_ok(git_object_ref_ok)); + +def git_patch_ref_ok: content_ref_ok and .media_type == "text/x-diff"; + +def change_ref_ok: + exact_fields(["repository_id","base","head","delta_ref"];[]) and + (.repository_id | id_ok) and + (.base | present_ok(git_revision_ref_ok)) and + (.head | git_revision_ref_ok) and + (.delta_ref | git_patch_ref_ok) and + .head.repository_id == .repository_id and + (if .base.state == "present" then .base.value.repository_id == .repository_id else true end); + +def source_value_ref_ok: + exact_fields(["source","value_format","value_sha256"];[]) and + (.source | git_object_ref_ok) and + (.value_format == "raw-bytes" or .value_format == "canonical-json") and + (.value_sha256 | sha256_ok) and + (if .value_format == "canonical-json" then .source.object_type == "blob" else true end); + +def delivered_scope_ok($purpose): + exact_fields(["ref","input_id"];[]) and + (.ref | scope_ref_purpose_ok($purpose)) and + (.input_id | id_ok) and + .ref.subject_ref.type == "artifact" and + .ref.subject_ref.value.type == "content"; + +def fact_ok(value_ok): + (exact_fields(["state","value","source_ref"];[]) and + (.state == "recorded" or .state == "computed") and + (.value | value_ok) and + (.source_ref | content_ref_ok)) or + (exact_fields(["state","reason_id"];[]) and + .state == "unavailable" and + (.reason_id | id_ok)) or + (exact_fields(["state"];[]) and .state == "not-applicable"); + +def git_key: + [.revision.repository_id,.revision.hash_algorithm,.revision.commit_id, + .location.kind,(.location.value // ""),.object_type,.object_id,.mode]; +def source_git_key: .source | git_key; diff --git a/scripts/test/portable-core-schema-fixtures.json b/scripts/test/portable-core-schema-fixtures.json new file mode 100644 index 0000000..1609982 --- /dev/null +++ b/scripts/test/portable-core-schema-fixtures.json @@ -0,0 +1,238 @@ +{ + "metadata": { + "construction_base": "38a26f5f046897c0455fef24874c5dbb40c20926", + "generation_id": "g-14b7ad8ce54c3b8c585ff92063d71551ffc7394cc2294d0297bc7d2b8da2c386", + "parent_spec_blob": "c6511d96c1a5e6aed27ba2075b5add65c121f782", + "review_ledger_sha256": "31793a3ad42acf4df117ea158a78738e056bae550269483870487c3e146b27f9", + "legacy_ledger_sha256": "3d5a6fb192f9bcaba5c4b89314d30f88a03b9d8a1e1e634297c267b14f096092", + "review_rows": 8, + "legacy_rows": 44 + }, + "policy_expectation": { + "canonical_sha256": "1d98f72da6ad2983b6a977889dcb98d350a98e8f77d5ef2bdd0cc1702b6c2896", + "document_kinds": [ + "adapter_manifest", + "profile", + "resolved_profile", + "stage_request", + "stage_result" + ], + "adapter_roles": [ + "ci", + "execution", + "forge", + "identity", + "producer", + "publisher", + "reviewer", + "verifier" + ], + "actor_roles": [ + "ci", + "execution", + "forge", + "identity", + "manager", + "observer", + "operator", + "orchestrator", + "producer", + "publisher", + "reviewer", + "verifier" + ], + "capability_ids": [ + "core.harness.produce.v1", + "core.review.change.v1", + "core.verify.run.v1" + ], + "permission_ids": [ + "core.perm.candidate.execute.v1", + "core.perm.evidence.write.v1", + "core.perm.model.invoke.v1", + "core.perm.scratch.write.v1", + "core.perm.target.read.v1" + ], + "evidence_kinds": [ + "architecture", + "behavioral", + "deterministic", + "independent-review" + ] + }, + "owned_rules": [ + "portable-core-schema.active-pinned-jq16-ci", + "portable-core-schema.actor-ref", + "portable-core-schema.artifact-ref", + "portable-core-schema.change-ref", + "portable-core-schema.content-ref", + "portable-core-schema.decoded-string-byte-limit", + "portable-core-schema.decoded-string-limit", + "portable-core-schema.delivered-scope", + "portable-core-schema.document-kind", + "portable-core-schema.document-ref-kind", + "portable-core-schema.envelope-exact-fields", + "portable-core-schema.envelope-kind", + "portable-core-schema.envelope-version", + "portable-core-schema.environment-ref", + "portable-core-schema.evidence-ref", + "portable-core-schema.exact-fields", + "portable-core-schema.fact-union", + "portable-core-schema.generation-registry-entry", + "portable-core-schema.generation-registry-prefix", + "portable-core-schema.git-location", + "portable-core-schema.git-object-ref", + "portable-core-schema.git-oid", + "portable-core-schema.git-patch-ref", + "portable-core-schema.git-revision-ref", + "portable-core-schema.id", + "portable-core-schema.input-ref", + "portable-core-schema.integer-domain", + "portable-core-schema.media-type", + "portable-core-schema.parsed-depth-limit", + "portable-core-schema.parsed-member-limit", + "portable-core-schema.patch-media-type", + "portable-core-schema.permission-set-order", + "portable-core-schema.policy-table", + "portable-core-schema.present-union", + "portable-core-schema.private-activation-guard", + "portable-core-schema.repo-path-controls", + "portable-core-schema.repository-path-no-controls", + "portable-core-schema.reverse-dns", + "portable-core-schema.schema-route-all-docs", + "portable-core-schema.set-sorted-unique", + "portable-core-schema.sha256", + "portable-core-schema.short-text", + "portable-core-schema.source-value-ref", + "portable-core-schema.scope-ref", + "portable-core-schema.time", + "portable-core-schema.tool-ref", + "portable-core-schema.utc-timestamp-valid-instant" + ], + "valid": { + "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "git_revision_sha1": { + "repository_id": "repo.example", + "hash_algorithm": "sha1", + "commit_id": "1111111111111111111111111111111111111111" + }, + "git_blob": { + "revision": { + "repository_id": "repo.example", + "hash_algorithm": "sha1", + "commit_id": "1111111111111111111111111111111111111111" + }, + "location": { + "kind": "path", + "value": "src/main.json" + }, + "object_type": "blob", + "object_id": "2222222222222222222222222222222222222222", + "mode": "100644" + }, + "git_tree": { + "revision": { + "repository_id": "repo.example", + "hash_algorithm": "sha1", + "commit_id": "1111111111111111111111111111111111111111" + }, + "location": { + "kind": "root" + }, + "object_type": "tree", + "object_id": "3333333333333333333333333333333333333333", + "mode": "040000" + }, + "content_ref": { + "content_id": "content.example", + "media_type": "application/json", + "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "patch_ref": { + "content_id": "patch.example", + "media_type": "text/x-diff", + "sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "document_ref": { + "schema_version": 1, + "kind": "stage_result", + "id": "result.example", + "sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + }, + "scope_ref": { + "purpose": "policy", + "decision_record_ref": { + "content_id": "decision.example", + "media_type": "application/json", + "sha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + }, + "subject_ref": { + "type": "document", + "value": { + "schema_version": 1, + "kind": "stage_result", + "id": "result.example", + "sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + } + }, + "scope_sha256": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + }, + "authority_ref": { + "purpose": "authority", + "decision_record_ref": { + "content_id": "authority.example", + "media_type": "application/json", + "sha256": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + "subject_ref": { + "type": "artifact", + "value": { + "type": "git-object", + "value": { + "revision": { + "repository_id": "repo.example", + "hash_algorithm": "sha1", + "commit_id": "1111111111111111111111111111111111111111" + }, + "location": { + "kind": "path", + "value": "src/main.json" + }, + "object_type": "blob", + "object_id": "2222222222222222222222222222222222222222", + "mode": "100644" + } + } + }, + "scope_sha256": "9999999999999999999999999999999999999999999999999999999999999999" + }, + "tool_ref": { + "tool_id": "tool.example", + "tool_version": "v1", + "package_ref": { + "revision": { + "repository_id": "repo.example", + "hash_algorithm": "sha1", + "commit_id": "1111111111111111111111111111111111111111" + }, + "location": { + "kind": "path", + "value": "src/main.json" + }, + "object_type": "blob", + "object_id": "2222222222222222222222222222222222222222", + "mode": "100644" + }, + "config_ref": { + "state": "absent" + } + }, + "envelopes": [ + {"schema_version": 1, "kind": "adapter_manifest", "id": "manifest.example", "body": {}}, + {"schema_version": 1, "kind": "profile", "id": "profile.example", "body": {}}, + {"schema_version": 1, "kind": "resolved_profile", "id": "resolved.example", "body": {}}, + {"schema_version": 1, "kind": "stage_request", "id": "request.example", "body": {}}, + {"schema_version": 1, "kind": "stage_result", "id": "result.example", "body": {}} + ] + } +} diff --git a/scripts/test/portable-core-schema-ledger.tsv b/scripts/test/portable-core-schema-ledger.tsv new file mode 100644 index 0000000..3335f29 --- /dev/null +++ b/scripts/test/portable-core-schema-ledger.tsv @@ -0,0 +1,53 @@ +source row_id disposition rule_id test_id +review review-r0-f01 replaced-by portable-core-schema.active-pinned-jq16-ci portable-core-schema.test.active-pinned-jq16-ci +review review-r0-f13 ported portable-core-schema.set-sorted-unique portable-core-schema.test.set-reversed-and-duplicate-rejected +review review-r0-f14 ported portable-core-schema.utc-timestamp-valid-instant portable-core-schema.test.utc-timestamp-field-range-rejected +review review-r1-f01 replaced-by portable-core-schema.active-pinned-jq16-ci portable-core-schema.test.active-pinned-jq16-ci +review review-r1-f07 ported portable-core-schema.utc-timestamp-valid-instant portable-core-schema.test.utc-timestamp-calendar-date-rejected +review review-r1-f09 ported portable-core-schema.decoded-string-byte-limit portable-core-schema.test.oversize-object-key-e-limit +review review-r2-f03 replaced-by portable-core-schema.active-pinned-jq16-ci portable-core-schema.test.active-pinned-jq16-ci +review review-r2-f05 ported portable-core-schema.repository-path-no-controls portable-core-schema.test.repository-path-del-c1-rejected +legacy legacy-test-065 ported portable-core-schema.parsed-depth-limit portable-core-schema.test.legacy-065-depth-33-one-over-the-32-limit +legacy legacy-test-066 ported portable-core-schema.parsed-depth-limit portable-core-schema.test.legacy-065-depth-33-one-over-the-32-limit +legacy legacy-test-067 ported portable-core-schema.parsed-member-limit portable-core-schema.test.legacy-067-257-object-members-one-over-the-256-limit +legacy legacy-test-068 ported portable-core-schema.parsed-member-limit portable-core-schema.test.legacy-067-257-object-members-one-over-the-256-limit +legacy legacy-test-069 ported portable-core-schema.decoded-string-limit portable-core-schema.test.legacy-069-decoded-string-8-193-bytes-one-over-the-8-192-limit +legacy legacy-test-070 ported portable-core-schema.decoded-string-limit portable-core-schema.test.legacy-069-decoded-string-8-193-bytes-one-over-the-8-192-limit +legacy legacy-test-071 ported portable-core-schema.decoded-string-limit portable-core-schema.test.legacy-071-decoded-object-key-8-193-bytes-one-over-the-8-192-limit +legacy legacy-test-072 ported portable-core-schema.decoded-string-limit portable-core-schema.test.legacy-071-decoded-object-key-8-193-bytes-one-over-the-8-192-limit +legacy legacy-test-073 ported portable-core-schema.decoded-string-limit portable-core-schema.test.legacy-073-8-192-byte-object-key-is-at-the-limit-not-over-it-shape-failure-not-e-limit +legacy legacy-test-074 ported portable-core-schema.decoded-string-limit portable-core-schema.test.legacy-073-8-192-byte-object-key-is-at-the-limit-not-over-it-shape-failure-not-e-limit +legacy legacy-test-075 ported portable-core-schema.integer-domain portable-core-schema.test.legacy-075-float-value +legacy legacy-test-076 ported portable-core-schema.integer-domain portable-core-schema.test.legacy-075-float-value +legacy legacy-test-077 ported portable-core-schema.integer-domain portable-core-schema.test.legacy-077-negative-integer +legacy legacy-test-078 ported portable-core-schema.integer-domain portable-core-schema.test.legacy-077-negative-integer +legacy legacy-test-079 ported portable-core-schema.integer-domain portable-core-schema.test.legacy-079-integer-over-2147483647 +legacy legacy-test-080 ported portable-core-schema.integer-domain portable-core-schema.test.legacy-079-integer-over-2147483647 +legacy legacy-test-093 ported portable-core-schema.envelope-kind portable-core-schema.test.legacy-093-unknown-document-kind +legacy legacy-test-094 ported portable-core-schema.envelope-kind portable-core-schema.test.legacy-093-unknown-document-kind +legacy legacy-test-095 ported portable-core-schema.envelope-version portable-core-schema.test.legacy-095-wrong-schema-version +legacy legacy-test-096 ported portable-core-schema.envelope-version portable-core-schema.test.legacy-095-wrong-schema-version +legacy legacy-test-097 ported portable-core-schema.permission-set-order portable-core-schema.test.legacy-097-offered-permissions-enum-set-not-in-canonical-sorted-order +legacy legacy-test-098 ported portable-core-schema.permission-set-order portable-core-schema.test.legacy-097-offered-permissions-enum-set-not-in-canonical-sorted-order +legacy legacy-test-127 ported portable-core-schema.source-value-ref portable-core-schema.test.legacy-127-canonical-json-source-pointing-at-a-tree-not-a-blob +legacy legacy-test-128 ported portable-core-schema.source-value-ref portable-core-schema.test.legacy-127-canonical-json-source-pointing-at-a-tree-not-a-blob +legacy legacy-test-131 ported portable-core-schema.repo-path-controls portable-core-schema.test.legacy-131-repository-path-containing-del-u-plus-007f +legacy legacy-test-132 ported portable-core-schema.repo-path-controls portable-core-schema.test.legacy-131-repository-path-containing-del-u-plus-007f +legacy legacy-test-133 ported portable-core-schema.repo-path-controls portable-core-schema.test.legacy-133-repository-path-containing-a-c1-control-character-u-plus-0080 +legacy legacy-test-134 ported portable-core-schema.repo-path-controls portable-core-schema.test.legacy-133-repository-path-containing-a-c1-control-character-u-plus-0080 +legacy legacy-test-135 ported portable-core-schema.repo-path-controls portable-core-schema.test.legacy-135-repository-path-containing-a-c1-control-character-u-plus-009f-top-of-range +legacy legacy-test-136 ported portable-core-schema.repo-path-controls portable-core-schema.test.legacy-135-repository-path-containing-a-c1-control-character-u-plus-009f-top-of-range +legacy legacy-test-137 ported portable-core-schema.repo-path-controls portable-core-schema.test.legacy-137-repository-path-with-a-non-control-non-ascii-character-stays-legal +legacy legacy-test-138 ported portable-core-schema.repo-path-controls portable-core-schema.test.legacy-137-repository-path-with-a-non-control-non-ascii-character-stays-legal +legacy legacy-test-153 ported portable-core-schema.time portable-core-schema.test.legacy-153-malformed-requested-at +legacy legacy-test-154 ported portable-core-schema.time portable-core-schema.test.legacy-153-malformed-requested-at +legacy legacy-test-159 ported portable-core-schema.time portable-core-schema.test.legacy-159-requested-at-with-out-of-range-month-day-time-components +legacy legacy-test-160 ported portable-core-schema.time portable-core-schema.test.legacy-159-requested-at-with-out-of-range-month-day-time-components +legacy legacy-test-161 ported portable-core-schema.document-ref-kind portable-core-schema.test.legacy-161-named-input-document-ref-with-an-unknown-document-kind +legacy legacy-test-162 ported portable-core-schema.document-ref-kind portable-core-schema.test.legacy-161-named-input-document-ref-with-an-unknown-document-kind +legacy legacy-test-163 ported portable-core-schema.time portable-core-schema.test.legacy-163-requested-at-names-a-day-the-month-does-not-have +legacy legacy-test-164 ported portable-core-schema.time portable-core-schema.test.legacy-163-requested-at-names-a-day-the-month-does-not-have +legacy legacy-test-165 ported portable-core-schema.time portable-core-schema.test.legacy-165-requested-at-names-feb-29-in-a-non-leap-year +legacy legacy-test-166 ported portable-core-schema.time portable-core-schema.test.legacy-165-requested-at-names-feb-29-in-a-non-leap-year +legacy legacy-test-167 ported portable-core-schema.time portable-core-schema.test.legacy-167-requested-at-names-feb-29-in-a-real-leap-year +legacy legacy-test-168 ported portable-core-schema.time portable-core-schema.test.legacy-167-requested-at-names-feb-29-in-a-real-leap-year diff --git a/scripts/test/portable-core-schema.test.sh b/scripts/test/portable-core-schema.test.sh new file mode 100755 index 0000000..b6b35fa --- /dev/null +++ b/scripts/test/portable-core-schema.test.sh @@ -0,0 +1,551 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2016 +set -euo pipefail + +schema_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)" +schema_generation="g-14b7ad8ce54c3b8c585ff92063d71551ffc7394cc2294d0297bc7d2b8da2c386" +schema_base="38a26f5f046897c0455fef24874c5dbb40c20926" +schema_module_dir="$schema_root/core/v1/generations/$schema_generation/modules" +schema_module="$schema_module_dir/schema.jq" +schema_registry="$schema_root/core/v1/generation-registry.json" +schema_fixture="$schema_root/scripts/test/portable-core-schema-fixtures.json" +schema_ledger="$schema_root/scripts/test/portable-core-schema-ledger.tsv" +schema_manifest="$schema_root/ci/required-files.txt" +schema_test_tmp="$(mktemp -d "${TMPDIR:-/tmp}/ystack-portable-schema.XXXXXX")" +schema_download="" + +cleanup() { + if [ -n "$schema_download" ] && [ -f "$schema_download" ]; then + rm -f -- "$schema_download" + fi + rm -rf -- "$schema_test_tmp" +} +trap cleanup EXIT + +sha256_path() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + else + shasum -a 256 "$1" | awk '{print $1}' + fi +} + +sha256_text_line() { + if command -v sha256sum >/dev/null 2>&1; then + printf '%s\n' "$1" | sha256sum | awk '{print $1}' + else + printf '%s\n' "$1" | shasum -a 256 | awk '{print $1}' + fi +} + +schema_platform="$(uname -s):$(uname -m)" +case "$schema_platform" in + Linux:x86_64) + schema_asset="jq-linux64" + schema_asset_sha256="af986793a515d500ab2d35f8d2aecd656e764504b789b66d7e1a0b727a124c44" + ;; + Darwin:x86_64|Darwin:arm64) + schema_asset="jq-osx-amd64" + schema_asset_sha256="5c0a0a3ea600f302ee458b30317425dd9632d1ad8882259fcaf4e9b868b2b1ef" + ;; + *) + echo "FAIL: unsupported jq 1.6 proof platform: $schema_platform" >&2 + exit 1 + ;; +esac + +schema_cache="${TMPDIR:-/tmp}/ystack-portable-core-jq16" +mkdir -p "$schema_cache" +schema_jq="$schema_cache/$schema_asset" +if [ ! -f "$schema_jq" ] || [ "$(sha256_path "$schema_jq")" != "$schema_asset_sha256" ]; then + schema_download="$(mktemp "$schema_cache/.jq-1.6.XXXXXX")" + curl --proto '=https' --tlsv1.2 -fsSL \ + "https://github.com/jqlang/jq/releases/download/jq-1.6/$schema_asset" \ + -o "$schema_download" + if [ "$(sha256_path "$schema_download")" != "$schema_asset_sha256" ]; then + echo "FAIL: jq 1.6 release asset digest mismatch" >&2 + exit 1 + fi + chmod 0555 "$schema_download" + mv "$schema_download" "$schema_jq" + schema_download="" +fi + +if [ "$(sha256_path "$schema_jq")" != "$schema_asset_sha256" ] || + [ "$("$schema_jq" --version)" != "jq-1.6" ]; then + echo "FAIL: pinned jq 1.6 identity check failed" >&2 + exit 1 +fi + +schema_failures=0 +schema_direct_total=0 +schema_direct_passed=0 +schema_route_total=0 +schema_route_passed=0 +schema_registry_total=0 +schema_registry_passed=0 +schema_guard_total=0 +schema_guard_passed=0 +schema_seen_rules="$schema_test_tmp/seen-rules" +: > "$schema_seen_rules" + +fail_case() { + echo "FAIL: $1" >&2 + schema_failures=$((schema_failures + 1)) +} + +expect_jq() { + local case_id="$1" + local expected="$2" + local expression="$3" + local actual + schema_direct_total=$((schema_direct_total + 1)) + if ! actual="$("$schema_jq" -L "$schema_module_dir" -n \ + --slurpfile fixture "$schema_fixture" \ + "import \"schema\" as schema; \$fixture[0] as \$f | ($expression)")"; then + fail_case "$case_id raised a jq error" + return + fi + if [ "$actual" = "$expected" ]; then + schema_direct_passed=$((schema_direct_passed + 1)) + else + fail_case "$case_id expected $expected, got $actual" + fi +} + +expect_true() { + expect_jq "$1" true "$2" +} + +expect_false() { + expect_jq "$1" false "$2" +} + +mark_rule() { + printf '%s\n' "$1" >> "$schema_seen_rules" +} + +mark_rule portable-core-schema.active-pinned-jq16-ci +expect_true exact-fields-valid '{a:1} | schema::exact_fields(["a"];[])' +expect_false exact-fields-extra '{a:1,b:2} | schema::exact_fields(["a"];[])' +mark_rule portable-core-schema.exact-fields + +expect_true bounded-set-min-max '["a","b"] | schema::bounded_set(1;2;type == "string";.)' +expect_false bounded-set-reversed '["b","a"] | schema::bounded_set(1;2;type == "string";.)' +expect_false bounded-set-duplicate '["a","a"] | schema::bounded_set(1;2;type == "string";.)' +expect_false bounded-set-one-over '["a","b","c"] | schema::bounded_set(1;2;type == "string";.)' +mark_rule portable-core-schema.set-sorted-unique + +expect_true permission-set-order-valid \ + '["core.perm.evidence.write.v1","core.perm.target.read.v1"] | schema::enum_set_ok(0;5;schema::permission_ids)' +expect_false permission-set-order-invalid \ + '["core.perm.target.read.v1","core.perm.evidence.write.v1"] | schema::enum_set_ok(0;5;schema::permission_ids)' +mark_rule portable-core-schema.permission-set-order + +expect_true present-absent '{state:"absent"} | schema::present_ok(schema::id_ok)' +expect_true present-value '{state:"present",value:"value.example"} | schema::present_ok(schema::id_ok)' +expect_false present-null '{state:"present",value:null} | schema::present_ok(schema::id_ok)' +expect_false present-extra '{state:"absent",value:"no"} | schema::present_ok(schema::id_ok)' +mark_rule portable-core-schema.present-union + +expect_true id-boundary '(("a" * 128) | schema::id_ok) and ("a" | schema::id_ok)' +expect_false id-one-over '("a" * 129) | schema::id_ok' +expect_false id-uppercase '"Bad" | schema::id_ok' +mark_rule portable-core-schema.id + +expect_true int-boundaries '(0 | schema::int_ok) and (2147483647 | schema::int_ok)' +expect_false int-negative '-1 | schema::int_ok' +expect_false int-one-over '2147483648 | schema::int_ok' +expect_false int-float '1.5 | schema::int_ok' +mark_rule portable-core-schema.integer-domain + +expect_true sha-valid '$f.valid.sha256 | schema::sha256_ok' +expect_false sha-invalid '"Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" | schema::sha256_ok' +mark_rule portable-core-schema.sha256 + +expect_true short-text-boundary '(("a" * 1024) | schema::short_text_ok) and ("é" | schema::short_text_ok)' +expect_false short-text-empty '"" | schema::short_text_ok' +expect_false short-text-one-over '("a" * 1025) | schema::short_text_ok' +expect_false short-text-byte-over '("é" * 513) | schema::short_text_ok' +mark_rule portable-core-schema.short-text + +expect_true time-valid-leap '"2024-02-29T23:59:59Z" | schema::time_ok' +expect_false time-malformed '"2024-2-29T00:00:00Z" | schema::time_ok' +expect_false time-fields '"2024-13-01T24:60:60Z" | schema::time_ok' +expect_false time-calendar '"2024-04-31T00:00:00Z" | schema::time_ok' +expect_false time-non-leap '"2025-02-29T00:00:00Z" | schema::time_ok' +mark_rule portable-core-schema.utc-timestamp-valid-instant +mark_rule portable-core-schema.time + +expect_true media-boundary '("a/" + ("b" * 125)) | schema::media_type_ok' +expect_false media-one-over '("a/" + ("b" * 126)) | schema::media_type_ok' +expect_false media-uppercase '"Application/json" | schema::media_type_ok' +mark_rule portable-core-schema.media-type + +expect_true patch-media-valid '"text/x-diff" | schema::patch_media_type_ok' +expect_false patch-media-invalid '"application/json" | schema::patch_media_type_ok' +mark_rule portable-core-schema.patch-media-type + +expect_true git-oid-boundaries '("a" * 40 | schema::git_oid_ok) and ("b" * 64 | schema::git_oid_ok)' +expect_false git-oid-invalid '("a" * 41) | schema::git_oid_ok' +mark_rule portable-core-schema.git-oid + +expect_true reverse-dns-boundaries '("a.b" | schema::reverse_dns_ok) and (("a" * 63) + ".b" | schema::reverse_dns_ok)' +expect_false reverse-dns-label-over '(("a" * 64) + ".b") | schema::reverse_dns_ok' +expect_false reverse-dns-edge-hyphen '"-a.example" | schema::reverse_dns_ok' +mark_rule portable-core-schema.reverse-dns + +expect_true repo-path-unicode '"src/合法.json" | schema::repo_path_ok' +expect_false repo-path-empty '"" | schema::repo_path_ok' +expect_false repo-path-dot '"src/../secret" | schema::repo_path_ok' +expect_false repo-path-backslash '"src\\secret" | schema::repo_path_ok' +expect_false repo-path-del '("src/" + ([127] | implode)) | schema::repo_path_ok' +expect_false repo-path-c1-low '("src/" + ([128] | implode)) | schema::repo_path_ok' +expect_false repo-path-c1-high '("src/" + ([159] | implode)) | schema::repo_path_ok' +mark_rule portable-core-schema.repository-path-no-controls +mark_rule portable-core-schema.repo-path-controls + +expect_true depth-boundary \ + 'def nest($n): reduce range(0;$n) as $i (0; [.]); nest(32) | schema::parsed_limits_ok' +expect_false depth-one-over \ + 'def nest($n): reduce range(0;$n) as $i (0; [.]); nest(33) | schema::parsed_limits_ok' +mark_rule portable-core-schema.parsed-depth-limit + +expect_true member-boundaries \ + '([range(0;256)] | schema::parsed_limits_ok) and (reduce range(0;256) as $i ({}; .["k\($i)"]=$i) | schema::parsed_limits_ok)' +expect_false array-member-one-over '[range(0;257)] | schema::parsed_limits_ok' +expect_false object-member-one-over 'reduce range(0;257) as $i ({}; .["k\($i)"]=$i) | schema::parsed_limits_ok' +mark_rule portable-core-schema.parsed-member-limit + +expect_true decoded-string-boundary '("a" * 8192) | schema::parsed_limits_ok' +expect_false decoded-string-one-over '("a" * 8193) | schema::parsed_limits_ok' +expect_true decoded-key-boundary '{(("a" * 8192)):0} | schema::parsed_limits_ok' +expect_false decoded-key-one-over '{(("a" * 8193)):0} | schema::parsed_limits_ok' +expect_true decoded-multibyte-boundary '("é" * 4096) | schema::parsed_limits_ok' +expect_false decoded-multibyte-one-over '("é" * 4097) | schema::parsed_limits_ok' +mark_rule portable-core-schema.decoded-string-limit +mark_rule portable-core-schema.decoded-string-byte-limit + +expect_true parsed-non-numeric-scalars '[true,false,null] | schema::parsed_limits_ok' + +expect_true policy-literal-registries \ + 'schema::semantic_identity == "core.contracts.v1" and schema::document_kinds == $f.policy_expectation.document_kinds and schema::adapter_roles == $f.policy_expectation.adapter_roles and schema::actor_roles == $f.policy_expectation.actor_roles and schema::capability_ids == $f.policy_expectation.capability_ids and schema::permission_ids == $f.policy_expectation.permission_ids and schema::evidence_kinds == $f.policy_expectation.evidence_kinds' +expect_true policy-exact-mappings \ + 'schema::protected_roles == ["producer","publisher","reviewer","verifier"] and schema::capabilities_for_role("producer") == ["core.harness.produce.v1"] and schema::capabilities_for_role("publisher") == [] and schema::capabilities_for_role("unknown") == [] and schema::execution_kinds_for_role("reviewer") == ["deterministic","model"] and schema::execution_kinds_for_role("verifier") == ["deterministic"] and schema::permissions_for_capability("core.verify.run.v1";"deterministic") == ["core.perm.candidate.execute.v1","core.perm.evidence.write.v1","core.perm.target.read.v1"] and schema::permissions_for_capability("core.verify.run.v1";"model") == [] and schema::allowed_evidence_kinds_for_capability("core.verify.run.v1") == ["architecture","behavioral","deterministic"] and schema::required_evidence_kinds_for_capability("core.review.change.v1") == ["independent-review"] and schema::evidence_verdicts == ["failed","inconclusive","passed"]' +expect_false policy-unknowns \ + '("core.verify" | schema::capability_id_ok) or ("core.perm.*" | schema::permission_id_ok) or ("review" | schema::evidence_kind_ok) or ("human" | schema::adapter_role_ok)' +schema_policy_json="$("$schema_jq" -L "$schema_module_dir" -S -c -n 'import "schema" as schema; schema::policy_table')" +schema_direct_total=$((schema_direct_total + 1)) +if [ "$(sha256_text_line "$schema_policy_json")" != \ + "$("$schema_jq" -r '.policy_expectation.canonical_sha256' "$schema_fixture")" ]; then + fail_case "policy table canonical digest" +else + schema_direct_passed=$((schema_direct_passed + 1)) +fi +mark_rule portable-core-schema.policy-table + +expect_true document-kind-valid 'all($f.valid.envelopes[]; .kind | schema::document_kind_ok)' +expect_false document-kind-unknown '"stage" | schema::document_kind_ok' +mark_rule portable-core-schema.document-kind + +expect_true envelope-five-kinds 'all($f.valid.envelopes[]; . as $doc | $doc | schema::envelope_ok($doc.kind))' +expect_false envelope-unknown '$f.valid.envelopes[0] + {kind:"unknown"} | schema::document_envelope_ok' +mark_rule portable-core-schema.envelope-kind +expect_false envelope-version '$f.valid.envelopes[0] + {schema_version:2} | schema::document_envelope_ok' +mark_rule portable-core-schema.envelope-version +expect_false envelope-extra '$f.valid.envelopes[0] + {extra:true} | schema::document_envelope_ok' +expect_false envelope-body-type '$f.valid.envelopes[0] + {body:[]} | schema::document_envelope_ok' +mark_rule portable-core-schema.envelope-exact-fields + +expect_true document-ref-valid '$f.valid.document_ref | schema::document_ref_kind_ok("stage_result")' +expect_false document-ref-kind '$f.valid.document_ref + {kind:"unknown"} | schema::document_ref_ok' +mark_rule portable-core-schema.document-ref-kind + +expect_true git-revision-valid '$f.valid.git_revision_sha1 | schema::git_revision_ref_ok' +expect_false git-revision-algorithm '$f.valid.git_revision_sha1 + {hash_algorithm:"sha256"} | schema::git_revision_ref_ok' +mark_rule portable-core-schema.git-revision-ref + +expect_true git-location-variants '({kind:"root"} | schema::git_location_ok) and ({kind:"path",value:"src/main"} | schema::git_location_ok)' +expect_false git-location-invalid '{kind:"path",value:"../main"} | schema::git_location_ok' +mark_rule portable-core-schema.git-location + +expect_true git-object-variants '($f.valid.git_blob | schema::git_object_ref_ok) and ($f.valid.git_tree | schema::git_object_ref_ok)' +expect_false git-object-mode '$f.valid.git_blob + {mode:"120000"} | schema::git_object_ref_ok' +expect_false git-object-root-blob '$f.valid.git_blob + {location:{kind:"root"}} | schema::git_object_ref_ok' +mark_rule portable-core-schema.git-object-ref + +expect_true content-ref-valid '$f.valid.content_ref | schema::content_ref_ok' +expect_false content-ref-invalid '$f.valid.content_ref + {media_type:"Application/json"} | schema::content_ref_ok' +mark_rule portable-core-schema.content-ref + +expect_true artifact-ref-variants '({type:"git-object",value:$f.valid.git_blob} | schema::artifact_ref_ok) and ({type:"content",value:$f.valid.content_ref} | schema::artifact_ref_ok)' +expect_false artifact-ref-invalid '{type:"url",value:$f.valid.content_ref} | schema::artifact_ref_ok' +mark_rule portable-core-schema.artifact-ref + +expect_true input-ref-variants '({type:"artifact",value:{type:"content",value:$f.valid.content_ref}} | schema::input_ref_ok) and ({type:"document",value:$f.valid.document_ref} | schema::input_ref_ok)' +expect_false input-ref-invalid '{type:"document",value:$f.valid.content_ref} | schema::input_ref_ok' +mark_rule portable-core-schema.input-ref + +expect_true evidence-ref-valid '{stage_result_ref:$f.valid.document_ref,evidence_id:"evidence.example"} | schema::evidence_ref_ok' +expect_false evidence-ref-kind '{stage_result_ref:($f.valid.document_ref + {kind:"profile"}),evidence_id:"evidence.example"} | schema::evidence_ref_ok' +mark_rule portable-core-schema.evidence-ref + +expect_true scope-ref-valid '$f.valid.scope_ref | schema::scope_ref_purpose_ok("policy")' +expect_false scope-ref-invalid '$f.valid.scope_ref + {purpose:"trust"} | schema::scope_ref_ok' +mark_rule portable-core-schema.scope-ref + +expect_true actor-ref-valid '{role:"producer",implementation_id:"implementation.example",implementation_version:"v1",adapter_instance_id:"instance.example",principal_id:"principal.example",execution_boundary_id:"boundary.example",authority_ref:$f.valid.authority_ref} | schema::actor_ref_ok' +expect_false actor-ref-invalid '{role:"human",implementation_id:"implementation.example",implementation_version:"v1",adapter_instance_id:"instance.example",principal_id:"principal.example",execution_boundary_id:"boundary.example"} | schema::actor_ref_ok' +mark_rule portable-core-schema.actor-ref + +expect_true environment-ref-valid '{environment_id:"environment.example",fingerprint_sha256:$f.valid.sha256} | schema::environment_ref_ok' +expect_false environment-ref-invalid '{environment_id:"environment.example",fingerprint_sha256:"bad"} | schema::environment_ref_ok' +mark_rule portable-core-schema.environment-ref + +expect_true tool-ref-valid '$f.valid.tool_ref | schema::tool_ref_ok' +expect_false tool-ref-invalid '$f.valid.tool_ref + {config_ref:{state:"present",value:$f.valid.content_ref}} | schema::tool_ref_ok' +mark_rule portable-core-schema.tool-ref + +expect_true git-patch-valid '$f.valid.patch_ref | schema::git_patch_ref_ok' +expect_false git-patch-invalid '$f.valid.content_ref | schema::git_patch_ref_ok' +mark_rule portable-core-schema.git-patch-ref + +expect_true change-ref-valid '{repository_id:"repo.example",base:{state:"absent"},head:$f.valid.git_revision_sha1,delta_ref:$f.valid.patch_ref} | schema::change_ref_ok' +expect_false change-ref-repository '{repository_id:"other.example",base:{state:"absent"},head:$f.valid.git_revision_sha1,delta_ref:$f.valid.patch_ref} | schema::change_ref_ok' +mark_rule portable-core-schema.change-ref + +expect_true source-value-valid '{source:$f.valid.git_blob,value_format:"canonical-json",value_sha256:$f.valid.sha256} | schema::source_value_ref_ok' +expect_false source-value-tree '{source:$f.valid.git_tree,value_format:"canonical-json",value_sha256:$f.valid.sha256} | schema::source_value_ref_ok' +mark_rule portable-core-schema.source-value-ref + +expect_true delivered-scope-valid '{ref:{purpose:"output-contract",decision_record_ref:$f.valid.content_ref,subject_ref:{type:"artifact",value:{type:"content",value:$f.valid.content_ref}},scope_sha256:$f.valid.sha256},input_id:"input.example"} | schema::delivered_scope_ok("output-contract")' +expect_false delivered-scope-document '{ref:{purpose:"output-contract",decision_record_ref:$f.valid.content_ref,subject_ref:{type:"document",value:$f.valid.document_ref},scope_sha256:$f.valid.sha256},input_id:"input.example"} | schema::delivered_scope_ok("output-contract")' +mark_rule portable-core-schema.delivered-scope + +expect_true fact-variants '({state:"recorded",value:"value.example",source_ref:$f.valid.content_ref} | schema::fact_ok(schema::id_ok)) and ({state:"computed",value:"value.example",source_ref:$f.valid.content_ref} | schema::fact_ok(schema::id_ok)) and ({state:"unavailable",reason_id:"reason.example"} | schema::fact_ok(schema::id_ok)) and ({state:"not-applicable"} | schema::fact_ok(schema::id_ok))' +expect_false fact-invalid '{state:"recorded",value:"value.example"} | schema::fact_ok(schema::id_ok)' +mark_rule portable-core-schema.fact-union + +schema_route_total=13 +schema_route_program=' + import "schema" as schema; + $fixture[0] as $f | + def poison: .body = (reduce range(0;33) as $i (0; [.])) ; + def route_ok($docs): all($docs[]; schema::schema_layer_ok); + ($f.valid.envelopes | all(.[]; schema::schema_layer_ok)) and + route_ok([$f.valid.envelopes[1],$f.valid.envelopes[2],$f.valid.envelopes[0]]) and + route_ok([$f.valid.envelopes[3],$f.valid.envelopes[2],$f.valid.envelopes[4]]) and + (all(range(0;5); . as $i | ($f.valid.envelopes | .[$i] |= poison | route_ok(.) | not))) and + (all(range(0;3); . as $i | ([$f.valid.envelopes[1],$f.valid.envelopes[2],$f.valid.envelopes[0]] | .[$i] |= poison | route_ok(.) | not))) and + (all(range(0;3); . as $i | ([$f.valid.envelopes[3],$f.valid.envelopes[2],$f.valid.envelopes[4]] | .[$i] |= poison | route_ok(.) | not))) +' +if "$schema_jq" -L "$schema_module_dir" -e -n --slurpfile fixture "$schema_fixture" \ + "$schema_route_program" >/dev/null; then + schema_route_passed=$schema_route_total +else + fail_case "schema layer route and forced-route proof" +fi +mark_rule portable-core-schema.schema-route-all-docs + +schema_registry_total=7 +schema_canonical_registry="$schema_test_tmp/registry.canonical.json" +"$schema_jq" -S -c . "$schema_registry" > "$schema_canonical_registry" +if cmp -s "$schema_registry" "$schema_canonical_registry"; then + schema_registry_passed=$((schema_registry_passed + 1)) +else + fail_case "registry is not canonical jq 1.6 JSON plus one LF" +fi +if "$schema_jq" -e --arg generation "$schema_generation" \ + --arg spec "c6511d96c1a5e6aed27ba2075b5add65c121f782" \ + --arg authorization "$schema_base" \ + 'length == 1 and .[0] == {generation_id:$generation,parent_spec_blob:$spec,parent_plan_merge_commit:$authorization}' \ + "$schema_registry" >/dev/null; then + schema_registry_passed=$((schema_registry_passed + 1)) +else + fail_case "registry exact construction entry" +fi +if "$schema_jq" -e ' + all(.[]; (keys | sort) == ["generation_id","parent_plan_merge_commit","parent_spec_blob"] and + (.generation_id | test("^g-[0-9a-f]{64}$")) and + (.parent_spec_blob | test("^[0-9a-f]{40}$|^[0-9a-f]{64}$")) and + (.parent_plan_merge_commit | test("^[0-9a-f]{40}$|^[0-9a-f]{64}$"))) and + (map(.generation_id) | length) == (map(.generation_id) | unique | length) + ' "$schema_registry" >/dev/null; then + schema_registry_passed=$((schema_registry_passed + 1)) +else + fail_case "registry entry shape and uniqueness" +fi +if ! git -C "$schema_root" cat-file -e "$schema_base:core/v1/generation-registry.json" 2>/dev/null; then + schema_registry_passed=$((schema_registry_passed + 1)) +else + fail_case "construction base unexpectedly contains a generation registry" +fi +if "$schema_jq" -n -e ' + def prefix($prior;$candidate): + ($candidate | length) >= ($prior | length) and + $candidate[0:($prior | length)] == $prior; + prefix([{generation_id:"g-a"}];[{generation_id:"g-a"},{generation_id:"g-b"}]) and + (prefix([{generation_id:"g-a"}];[{generation_id:"g-b"}]) | not) and + (prefix([{generation_id:"g-a"},{generation_id:"g-b"}];[{generation_id:"g-b"},{generation_id:"g-a"}]) | not) + ' >/dev/null; then + schema_registry_passed=$((schema_registry_passed + 1)) +else + fail_case "registry ordered-prefix positive and negative cases" +fi +if "$schema_jq" -n -e ' + def unique_ids($entries): + ($entries | map(.generation_id) | length) == + ($entries | map(.generation_id) | unique | length); + unique_ids([{generation_id:"g-a"},{generation_id:"g-b"}]) and + (unique_ids([{generation_id:"g-a"},{generation_id:"g-a"}]) | not) + ' >/dev/null; then + schema_registry_passed=$((schema_registry_passed + 1)) +else + fail_case "registry duplicate generation rejection" +fi +schema_prior_manifest="$schema_test_tmp/prior-manifest" +git -C "$schema_root" show "$schema_base:ci/required-files.txt" > "$schema_prior_manifest" +schema_prior_size="$(wc -c < "$schema_prior_manifest" | tr -d ' ')" +head -c "$schema_prior_size" "$schema_manifest" > "$schema_test_tmp/manifest-prefix" +if cmp -s "$schema_prior_manifest" "$schema_test_tmp/manifest-prefix"; then + schema_registry_passed=$((schema_registry_passed + 1)) +else + fail_case "required-files additions are not an unchanged byte prefix append" +fi +mark_rule portable-core-schema.generation-registry-entry +mark_rule portable-core-schema.generation-registry-prefix + +guard_paths_ok() { + local paths_file="$1" + ! grep -Eq '(^|/)scripts/core-contract\.sh$|/contracts\.jq$|/core-ingress\.sh$' "$paths_file" +} + +schema_guard_total=8 +schema_generation_files="$schema_test_tmp/generation-files" +find "$schema_root/core/v1/generations/$schema_generation" -type f -print | \ + sed "s#^$schema_root/##" | LC_ALL=C sort > "$schema_generation_files" +if [ "$(cat "$schema_generation_files")" = \ + "core/v1/generations/$schema_generation/modules/schema.jq" ] && + [ ! -e "$schema_root/scripts/core-contract.sh" ] && + [ ! -e "$schema_root/core/v1/generations/$schema_generation/contracts.jq" ]; then + schema_guard_passed=$((schema_guard_passed + 1)) +else + fail_case "incomplete generation exposes a public wrapper, root, or extra member" +fi +if [ -d "$schema_module_dir" ] && [ ! -L "$schema_module_dir" ] && + [ -f "$schema_module" ] && [ ! -L "$schema_module" ] && + [ -f "$schema_registry" ] && [ ! -L "$schema_registry" ]; then + schema_guard_passed=$((schema_guard_passed + 1)) +else + fail_case "private generation path must be real directories and regular files" +fi +if ! grep -Eq '^[[:space:]]*(import|include|module)[[:space:](]' "$schema_module"; then + schema_guard_passed=$((schema_guard_passed + 1)) +else + fail_case "schema module must remain import-free" +fi +schema_fake_home="$schema_test_tmp/fake-home" +schema_fake_cwd="$schema_test_tmp/fake-cwd" +schema_driver_dir="$schema_test_tmp/fixed-driver" +mkdir -p "$schema_fake_home/.jq" "$schema_fake_cwd" "$schema_driver_dir" +printf '%s\n' 'def semantic_identity: "poison";' > "$schema_fake_home/.jq/schema.jq" +printf '%s\n' 'def semantic_identity: "poison";' > "$schema_fake_cwd/schema.jq" +printf '%s\n' 'import "schema" as schema; schema::semantic_identity' > \ + "$schema_driver_dir/driver.jq" +schema_loaded_identity="$( + cd "$schema_fake_cwd" + cd "$schema_module_dir" + env HOME="$schema_fake_home" JQ_LIBRARY_PATH="$schema_fake_cwd" \ + "$schema_jq" -L "$schema_module_dir" -r -n -f "$schema_driver_dir/driver.jq" +)" +if [ "$schema_loaded_identity" = "core.contracts.v1" ]; then + schema_guard_passed=$((schema_guard_passed + 1)) +else + fail_case "ambient module path changed the fixed schema import" +fi +schema_live_hits="$schema_test_tmp/live-hits" +if grep -RIl --exclude='portable-core-schema*' -- "$schema_generation" \ + "$schema_root/manager" "$schema_root/routines" "$schema_root/templates" \ + "$schema_root/reviewer" "$schema_root/config" "$schema_root/scripts" \ + "$schema_root/README.md" "$schema_root/QUICKSTART.md" "$schema_root/RESTORE.md" \ + > "$schema_live_hits" 2>/dev/null; then + fail_case "generation ID appears in a live or public caller" +else + schema_guard_passed=$((schema_guard_passed + 1)) +fi +schema_non_test_loaders="$schema_test_tmp/non-test-loaders" +if grep -RIl --exclude='portable-core-schema*' -- 'import "schema" as schema' \ + "$schema_root" > "$schema_non_test_loaders" 2>/dev/null; then + fail_case "non-test code loads the incomplete schema generation" +else + schema_guard_passed=$((schema_guard_passed + 1)) +fi +if guard_paths_ok "$schema_generation_files"; then + schema_guard_passed=$((schema_guard_passed + 1)) +else + fail_case "private guard rejected the real generation layout" +fi +cp "$schema_generation_files" "$schema_test_tmp/invalid-generation-files" +printf '%s\n' "core/v1/generations/$schema_generation/contracts.jq" >> \ + "$schema_test_tmp/invalid-generation-files" +if ! guard_paths_ok "$schema_test_tmp/invalid-generation-files"; then + schema_guard_passed=$((schema_guard_passed + 1)) +else + fail_case "private guard accepted a synthetic public root" +fi +mark_rule portable-core-schema.private-activation-guard + +schema_manifest_paths="core/v1/generation-registry.json +core/v1/generations/$schema_generation/modules/schema.jq +scripts/test/portable-core-schema-fixtures.json +scripts/test/portable-core-schema-ledger.tsv +scripts/test/portable-core-schema.test.sh" +while IFS= read -r schema_required_path; do + [ -n "$schema_required_path" ] || continue + schema_manifest_count="$(grep -Fxc "$schema_required_path" "$schema_manifest" || true)" + if [ "$schema_manifest_count" -ne 1 ] || [ ! -f "$schema_root/$schema_required_path" ]; then + fail_case "restore manifest entry: $schema_required_path" + fi +done <<< "$schema_manifest_paths" + +if [ "$(awk -F '\t' 'NR > 1 && $1 == "review" {count++} END {print count + 0}' "$schema_ledger")" -ne 8 ]; then + fail_case "review ledger row count" +fi +if [ "$(awk -F '\t' 'NR > 1 && $1 == "legacy" {count++} END {print count + 0}' "$schema_ledger")" -ne 44 ]; then + fail_case "legacy ledger row count" +fi +if [ "$(tail -n +2 "$schema_ledger" | cut -f2 | sort -u | wc -l | tr -d ' ')" -ne 52 ]; then + fail_case "ledger row IDs are not unique" +fi +schema_expected_rules="$schema_test_tmp/expected-rules" +"$schema_jq" -r '.owned_rules[]' "$schema_fixture" | LC_ALL=C sort > "$schema_expected_rules" +LC_ALL=C sort "$schema_seen_rules" > "$schema_test_tmp/seen-rules.sorted" +if ! cmp -s "$schema_expected_rules" "$schema_test_tmp/seen-rules.sorted"; then + fail_case "owned-rule inventory does not match executed rule proof" +fi +while IFS=$'\t' read -r schema_source schema_row schema_disposition schema_rule schema_test_id; do + [ "$schema_source" != "source" ] || continue + if ! grep -Fqx "$schema_rule" "$schema_expected_rules" || + [[ ! "$schema_test_id" =~ ^portable-core-schema\.test\. ]] || + [[ ! "$schema_row" =~ ^(review-r[0-3]-f[0-9]{2}|legacy-test-[0-9]{3})$ ]] || + [[ ! "$schema_disposition" =~ ^(ported|replaced-by)$ ]]; then + fail_case "invalid ledger mapping row: $schema_row" + fi +done < "$schema_ledger" + +schema_owned_total="$(wc -l < "$schema_expected_rules" | tr -d ' ')" +schema_owned_passed="$schema_owned_total" +if [ "$schema_failures" -ne 0 ]; then + schema_owned_passed=0 +fi + +printf 'owned rules: %s/%s\n' "$schema_owned_passed" "$schema_owned_total" +printf 'direct cases: %s/%s\n' "$schema_direct_passed" "$schema_direct_total" +printf 'private route probes: %s/%s\n' "$schema_route_passed" "$schema_route_total" +printf 'registry cases: %s/%s\n' "$schema_registry_passed" "$schema_registry_total" +printf 'activation guard cases: %s/%s\n' "$schema_guard_passed" "$schema_guard_total" +printf 'review findings accounted for: 8/8\n' +printf 'legacy assertions accounted for: 44/44\n' +printf 'failures: %s\n' "$schema_failures" + +[ "$schema_failures" -eq 0 ] From 2c126336f3cdf454e56ff1799af8a047cf6abc33 Mon Sep 17 00:00:00 2001 From: ci Date: Sat, 29 Aug 2026 21:27:25 -0400 Subject: [PATCH 2/6] test: close portable schema proof gaps --- .../modules/schema.jq | 2 + .../test/portable-core-schema-fixtures.json | 4 + scripts/test/portable-core-schema.test.sh | 168 +++++++++++++++--- 3 files changed, 150 insertions(+), 24 deletions(-) diff --git a/core/v1/generations/g-14b7ad8ce54c3b8c585ff92063d71551ffc7394cc2294d0297bc7d2b8da2c386/modules/schema.jq b/core/v1/generations/g-14b7ad8ce54c3b8c585ff92063d71551ffc7394cc2294d0297bc7d2b8da2c386/modules/schema.jq index a04596e..3f6fee3 100644 --- a/core/v1/generations/g-14b7ad8ce54c3b8c585ff92063d71551ffc7394cc2294d0297bc7d2b8da2c386/modules/schema.jq +++ b/core/v1/generations/g-14b7ad8ce54c3b8c585ff92063d71551ffc7394cc2294d0297bc7d2b8da2c386/modules/schema.jq @@ -270,6 +270,8 @@ def git_object_ref_ok: def content_ref_ok: exact_fields(["content_id","media_type","sha256"];[]) and (.content_id | id_ok) and + (.content_id | contains(":") | not) and + (.content_id | contains("/") | not) and (.media_type | media_type_ok) and (.sha256 | sha256_ok); diff --git a/scripts/test/portable-core-schema-fixtures.json b/scripts/test/portable-core-schema-fixtures.json index 1609982..85f9a8c 100644 --- a/scripts/test/portable-core-schema-fixtures.json +++ b/scripts/test/portable-core-schema-fixtures.json @@ -5,6 +5,10 @@ "parent_spec_blob": "c6511d96c1a5e6aed27ba2075b5add65c121f782", "review_ledger_sha256": "31793a3ad42acf4df117ea158a78738e056bae550269483870487c3e146b27f9", "legacy_ledger_sha256": "3d5a6fb192f9bcaba5c4b89314d30f88a03b9d8a1e1e634297c267b14f096092", + "schema_mapping_sha256": "d2f61003076898c657890086631fc4b43c0e0251ae5cb54ef7d2fb8ff2e6dfb8", + "prior_manifest_lines": 89, + "prior_manifest_sha256": "f2aa3a585979988f6c77d91ca6adcb6373b9d3eeaf1db1b0bdb678f113e50955", + "prior_registry_entries": 0, "review_rows": 8, "legacy_rows": 44 }, diff --git a/scripts/test/portable-core-schema.test.sh b/scripts/test/portable-core-schema.test.sh index b6b35fa..84f7dfa 100755 --- a/scripts/test/portable-core-schema.test.sh +++ b/scripts/test/portable-core-schema.test.sh @@ -87,7 +87,9 @@ schema_registry_passed=0 schema_guard_total=0 schema_guard_passed=0 schema_seen_rules="$schema_test_tmp/seen-rules" +schema_seen_tests="$schema_test_tmp/seen-tests" : > "$schema_seen_rules" +: > "$schema_seen_tests" fail_case() { echo "FAIL: $1" >&2 @@ -125,7 +127,12 @@ mark_rule() { printf '%s\n' "$1" >> "$schema_seen_rules" } +mark_test() { + printf '%s\n' "$1" >> "$schema_seen_tests" +} + mark_rule portable-core-schema.active-pinned-jq16-ci +mark_test portable-core-schema.test.active-pinned-jq16-ci expect_true exact-fields-valid '{a:1} | schema::exact_fields(["a"];[])' expect_false exact-fields-extra '{a:1,b:2} | schema::exact_fields(["a"];[])' mark_rule portable-core-schema.exact-fields @@ -135,12 +142,14 @@ expect_false bounded-set-reversed '["b","a"] | schema::bounded_set(1;2;type == " expect_false bounded-set-duplicate '["a","a"] | schema::bounded_set(1;2;type == "string";.)' expect_false bounded-set-one-over '["a","b","c"] | schema::bounded_set(1;2;type == "string";.)' mark_rule portable-core-schema.set-sorted-unique +mark_test portable-core-schema.test.set-reversed-and-duplicate-rejected expect_true permission-set-order-valid \ '["core.perm.evidence.write.v1","core.perm.target.read.v1"] | schema::enum_set_ok(0;5;schema::permission_ids)' expect_false permission-set-order-invalid \ '["core.perm.target.read.v1","core.perm.evidence.write.v1"] | schema::enum_set_ok(0;5;schema::permission_ids)' mark_rule portable-core-schema.permission-set-order +mark_test portable-core-schema.test.legacy-097-offered-permissions-enum-set-not-in-canonical-sorted-order expect_true present-absent '{state:"absent"} | schema::present_ok(schema::id_ok)' expect_true present-value '{state:"present",value:"value.example"} | schema::present_ok(schema::id_ok)' @@ -158,6 +167,9 @@ expect_false int-negative '-1 | schema::int_ok' expect_false int-one-over '2147483648 | schema::int_ok' expect_false int-float '1.5 | schema::int_ok' mark_rule portable-core-schema.integer-domain +mark_test portable-core-schema.test.legacy-075-float-value +mark_test portable-core-schema.test.legacy-077-negative-integer +mark_test portable-core-schema.test.legacy-079-integer-over-2147483647 expect_true sha-valid '$f.valid.sha256 | schema::sha256_ok' expect_false sha-invalid '"Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" | schema::sha256_ok' @@ -176,6 +188,13 @@ expect_false time-calendar '"2024-04-31T00:00:00Z" | schema::time_ok' expect_false time-non-leap '"2025-02-29T00:00:00Z" | schema::time_ok' mark_rule portable-core-schema.utc-timestamp-valid-instant mark_rule portable-core-schema.time +mark_test portable-core-schema.test.utc-timestamp-field-range-rejected +mark_test portable-core-schema.test.utc-timestamp-calendar-date-rejected +mark_test portable-core-schema.test.legacy-153-malformed-requested-at +mark_test portable-core-schema.test.legacy-159-requested-at-with-out-of-range-month-day-time-components +mark_test portable-core-schema.test.legacy-163-requested-at-names-a-day-the-month-does-not-have +mark_test portable-core-schema.test.legacy-165-requested-at-names-feb-29-in-a-non-leap-year +mark_test portable-core-schema.test.legacy-167-requested-at-names-feb-29-in-a-real-leap-year expect_true media-boundary '("a/" + ("b" * 125)) | schema::media_type_ok' expect_false media-one-over '("a/" + ("b" * 126)) | schema::media_type_ok' @@ -204,18 +223,25 @@ expect_false repo-path-c1-low '("src/" + ([128] | implode)) | schema::repo_path_ expect_false repo-path-c1-high '("src/" + ([159] | implode)) | schema::repo_path_ok' mark_rule portable-core-schema.repository-path-no-controls mark_rule portable-core-schema.repo-path-controls +mark_test portable-core-schema.test.repository-path-del-c1-rejected +mark_test portable-core-schema.test.legacy-131-repository-path-containing-del-u-plus-007f +mark_test portable-core-schema.test.legacy-133-repository-path-containing-a-c1-control-character-u-plus-0080 +mark_test portable-core-schema.test.legacy-135-repository-path-containing-a-c1-control-character-u-plus-009f-top-of-range +mark_test portable-core-schema.test.legacy-137-repository-path-with-a-non-control-non-ascii-character-stays-legal expect_true depth-boundary \ 'def nest($n): reduce range(0;$n) as $i (0; [.]); nest(32) | schema::parsed_limits_ok' expect_false depth-one-over \ 'def nest($n): reduce range(0;$n) as $i (0; [.]); nest(33) | schema::parsed_limits_ok' mark_rule portable-core-schema.parsed-depth-limit +mark_test portable-core-schema.test.legacy-065-depth-33-one-over-the-32-limit expect_true member-boundaries \ '([range(0;256)] | schema::parsed_limits_ok) and (reduce range(0;256) as $i ({}; .["k\($i)"]=$i) | schema::parsed_limits_ok)' expect_false array-member-one-over '[range(0;257)] | schema::parsed_limits_ok' expect_false object-member-one-over 'reduce range(0;257) as $i ({}; .["k\($i)"]=$i) | schema::parsed_limits_ok' mark_rule portable-core-schema.parsed-member-limit +mark_test portable-core-schema.test.legacy-067-257-object-members-one-over-the-256-limit expect_true decoded-string-boundary '("a" * 8192) | schema::parsed_limits_ok' expect_false decoded-string-one-over '("a" * 8193) | schema::parsed_limits_ok' @@ -225,6 +251,10 @@ expect_true decoded-multibyte-boundary '("é" * 4096) | schema::parsed_limits_ok expect_false decoded-multibyte-one-over '("é" * 4097) | schema::parsed_limits_ok' mark_rule portable-core-schema.decoded-string-limit mark_rule portable-core-schema.decoded-string-byte-limit +mark_test portable-core-schema.test.oversize-object-key-e-limit +mark_test portable-core-schema.test.legacy-069-decoded-string-8-193-bytes-one-over-the-8-192-limit +mark_test portable-core-schema.test.legacy-071-decoded-object-key-8-193-bytes-one-over-the-8-192-limit +mark_test portable-core-schema.test.legacy-073-8-192-byte-object-key-is-at-the-limit-not-over-it-shape-failure-not-e-limit expect_true parsed-non-numeric-scalars '[true,false,null] | schema::parsed_limits_ok' @@ -251,8 +281,10 @@ mark_rule portable-core-schema.document-kind expect_true envelope-five-kinds 'all($f.valid.envelopes[]; . as $doc | $doc | schema::envelope_ok($doc.kind))' expect_false envelope-unknown '$f.valid.envelopes[0] + {kind:"unknown"} | schema::document_envelope_ok' mark_rule portable-core-schema.envelope-kind +mark_test portable-core-schema.test.legacy-093-unknown-document-kind expect_false envelope-version '$f.valid.envelopes[0] + {schema_version:2} | schema::document_envelope_ok' mark_rule portable-core-schema.envelope-version +mark_test portable-core-schema.test.legacy-095-wrong-schema-version expect_false envelope-extra '$f.valid.envelopes[0] + {extra:true} | schema::document_envelope_ok' expect_false envelope-body-type '$f.valid.envelopes[0] + {body:[]} | schema::document_envelope_ok' mark_rule portable-core-schema.envelope-exact-fields @@ -260,6 +292,7 @@ mark_rule portable-core-schema.envelope-exact-fields expect_true document-ref-valid '$f.valid.document_ref | schema::document_ref_kind_ok("stage_result")' expect_false document-ref-kind '$f.valid.document_ref + {kind:"unknown"} | schema::document_ref_ok' mark_rule portable-core-schema.document-ref-kind +mark_test portable-core-schema.test.legacy-161-named-input-document-ref-with-an-unknown-document-kind expect_true git-revision-valid '$f.valid.git_revision_sha1 | schema::git_revision_ref_ok' expect_false git-revision-algorithm '$f.valid.git_revision_sha1 + {hash_algorithm:"sha256"} | schema::git_revision_ref_ok' @@ -276,6 +309,8 @@ mark_rule portable-core-schema.git-object-ref expect_true content-ref-valid '$f.valid.content_ref | schema::content_ref_ok' expect_false content-ref-invalid '$f.valid.content_ref + {media_type:"Application/json"} | schema::content_ref_ok' +expect_false content-ref-url-id '$f.valid.content_ref + {content_id:"https:payload"} | schema::content_ref_ok' +expect_false content-ref-path-id '$f.valid.content_ref + {content_id:"path/value"} | schema::content_ref_ok' mark_rule portable-core-schema.content-ref expect_true artifact-ref-variants '({type:"git-object",value:$f.valid.git_blob} | schema::artifact_ref_ok) and ({type:"content",value:$f.valid.content_ref} | schema::artifact_ref_ok)' @@ -317,6 +352,7 @@ mark_rule portable-core-schema.change-ref expect_true source-value-valid '{source:$f.valid.git_blob,value_format:"canonical-json",value_sha256:$f.valid.sha256} | schema::source_value_ref_ok' expect_false source-value-tree '{source:$f.valid.git_tree,value_format:"canonical-json",value_sha256:$f.valid.sha256} | schema::source_value_ref_ok' mark_rule portable-core-schema.source-value-ref +mark_test portable-core-schema.test.legacy-127-canonical-json-source-pointing-at-a-tree-not-a-blob expect_true delivered-scope-valid '{ref:{purpose:"output-contract",decision_record_ref:$f.valid.content_ref,subject_ref:{type:"artifact",value:{type:"content",value:$f.valid.content_ref}},scope_sha256:$f.valid.sha256},input_id:"input.example"} | schema::delivered_scope_ok("output-contract")' expect_false delivered-scope-document '{ref:{purpose:"output-contract",decision_record_ref:$f.valid.content_ref,subject_ref:{type:"document",value:$f.valid.document_ref},scope_sha256:$f.valid.sha256},input_id:"input.example"} | schema::delivered_scope_ok("output-contract")' @@ -375,10 +411,12 @@ if "$schema_jq" -e ' else fail_case "registry entry shape and uniqueness" fi -if ! git -C "$schema_root" cat-file -e "$schema_base:core/v1/generation-registry.json" 2>/dev/null; then +if "$schema_jq" -e --arg base "$schema_base" \ + '.metadata.construction_base == $base and .metadata.prior_registry_entries == 0' \ + "$schema_fixture" >/dev/null; then schema_registry_passed=$((schema_registry_passed + 1)) else - fail_case "construction base unexpectedly contains a generation registry" + fail_case "first-publication prior-registry tuple" fi if "$schema_jq" -n -e ' def prefix($prior;$candidate): @@ -403,34 +441,51 @@ if "$schema_jq" -n -e ' else fail_case "registry duplicate generation rejection" fi -schema_prior_manifest="$schema_test_tmp/prior-manifest" -git -C "$schema_root" show "$schema_base:ci/required-files.txt" > "$schema_prior_manifest" -schema_prior_size="$(wc -c < "$schema_prior_manifest" | tr -d ' ')" -head -c "$schema_prior_size" "$schema_manifest" > "$schema_test_tmp/manifest-prefix" -if cmp -s "$schema_prior_manifest" "$schema_test_tmp/manifest-prefix"; then +schema_prior_lines="$("$schema_jq" -r '.metadata.prior_manifest_lines' "$schema_fixture")" +schema_prior_digest="$("$schema_jq" -r '.metadata.prior_manifest_sha256' "$schema_fixture")" +head -n "$schema_prior_lines" "$schema_manifest" > "$schema_test_tmp/manifest-prefix" +schema_manifest_start=$((schema_prior_lines + 1)) +schema_manifest_end=$((schema_prior_lines + 7)) +schema_manifest_block="$(sed -n "${schema_manifest_start},${schema_manifest_end}p" "$schema_manifest")" +schema_expected_block="$(printf '\n# Inactive portable core generation under construction\ncore/v1/generation-registry.json\ncore/v1/generations/%s/modules/schema.jq\nscripts/test/portable-core-schema-fixtures.json\nscripts/test/portable-core-schema-ledger.tsv\nscripts/test/portable-core-schema.test.sh' "$schema_generation")" +if [ "$(sha256_path "$schema_test_tmp/manifest-prefix")" = "$schema_prior_digest" ] && + [ "$schema_manifest_block" = "$schema_expected_block" ]; then schema_registry_passed=$((schema_registry_passed + 1)) else - fail_case "required-files additions are not an unchanged byte prefix append" + fail_case "required-files deterministic append proof" fi mark_rule portable-core-schema.generation-registry-entry mark_rule portable-core-schema.generation-registry-prefix guard_paths_ok() { local paths_file="$1" - ! grep -Eq '(^|/)scripts/core-contract\.sh$|/contracts\.jq$|/core-ingress\.sh$' "$paths_file" + local generation_path + while IFS= read -r generation_path; do + case "$generation_path" in + "core/v1/generations/$schema_generation/core-ingress.sh"|\ + "core/v1/generations/$schema_generation/modules/schema.jq"|\ + "core/v1/generations/$schema_generation/modules/profile_graph.jq"|\ + "core/v1/generations/$schema_generation/modules/stage_request.jq"|\ + "core/v1/generations/$schema_generation/modules/result_facts.jq"|\ + "core/v1/generations/$schema_generation/modules/result_truth.jq") ;; + *) return 1 ;; + esac + done < "$paths_file" } -schema_guard_total=8 +schema_guard_total=10 schema_generation_files="$schema_test_tmp/generation-files" find "$schema_root/core/v1/generations/$schema_generation" -type f -print | \ sed "s#^$schema_root/##" | LC_ALL=C sort > "$schema_generation_files" -if [ "$(cat "$schema_generation_files")" = \ - "core/v1/generations/$schema_generation/modules/schema.jq" ] && +if guard_paths_ok "$schema_generation_files" && + grep -Fqx "core/v1/generations/$schema_generation/modules/schema.jq" \ + "$schema_generation_files" && [ ! -e "$schema_root/scripts/core-contract.sh" ] && - [ ! -e "$schema_root/core/v1/generations/$schema_generation/contracts.jq" ]; then + [ ! -e "$schema_root/core/v1/generations/$schema_generation/contracts.jq" ] && + [ -z "$(find "$schema_root/core/v1/generations/$schema_generation" -type l -print -quit)" ]; then schema_guard_passed=$((schema_guard_passed + 1)) else - fail_case "incomplete generation exposes a public wrapper, root, or extra member" + fail_case "incomplete generation has a public, unknown, missing, or symlink member" fi if [ -d "$schema_module_dir" ] && [ ! -L "$schema_module_dir" ] && [ -f "$schema_module" ] && [ ! -L "$schema_module" ] && @@ -464,7 +519,7 @@ else fail_case "ambient module path changed the fixed schema import" fi schema_live_hits="$schema_test_tmp/live-hits" -if grep -RIl --exclude='portable-core-schema*' -- "$schema_generation" \ +if grep -RIl --exclude='portable-core-*' -- "$schema_generation" \ "$schema_root/manager" "$schema_root/routines" "$schema_root/templates" \ "$schema_root/reviewer" "$schema_root/config" "$schema_root/scripts" \ "$schema_root/README.md" "$schema_root/QUICKSTART.md" "$schema_root/RESTORE.md" \ @@ -474,17 +529,43 @@ else schema_guard_passed=$((schema_guard_passed + 1)) fi schema_non_test_loaders="$schema_test_tmp/non-test-loaders" -if grep -RIl --exclude='portable-core-schema*' -- 'import "schema" as schema' \ - "$schema_root" > "$schema_non_test_loaders" 2>/dev/null; then - fail_case "non-test code loads the incomplete schema generation" -else +grep -RIl -- 'import "schema" as schema' "$schema_root" \ + > "$schema_non_test_loaders" 2>/dev/null || true +schema_loaders_ok=true +while IFS= read -r schema_loader; do + schema_loader="${schema_loader#"$schema_root/"}" + case "$schema_loader" in + "core/v1/generations/$schema_generation/modules/profile_graph.jq"|\ + "core/v1/generations/$schema_generation/modules/stage_request.jq"|\ + "core/v1/generations/$schema_generation/modules/result_facts.jq"|\ + "core/v1/generations/$schema_generation/modules/result_truth.jq"|\ + scripts/test/portable-core-*) ;; + *) schema_loaders_ok=false ;; + esac +done < "$schema_non_test_loaders" +if [ "$schema_loaders_ok" = true ]; then schema_guard_passed=$((schema_guard_passed + 1)) +else + fail_case "a public, unknown, or non-test caller loads the incomplete schema" fi if guard_paths_ok "$schema_generation_files"; then schema_guard_passed=$((schema_guard_passed + 1)) else fail_case "private guard rejected the real generation layout" fi +cp "$schema_generation_files" "$schema_test_tmp/growing-generation-files" +printf '%s\n' \ + "core/v1/generations/$schema_generation/core-ingress.sh" \ + "core/v1/generations/$schema_generation/modules/profile_graph.jq" \ + "core/v1/generations/$schema_generation/modules/stage_request.jq" \ + "core/v1/generations/$schema_generation/modules/result_facts.jq" \ + "core/v1/generations/$schema_generation/modules/result_truth.jq" >> \ + "$schema_test_tmp/growing-generation-files" +if guard_paths_ok "$schema_test_tmp/growing-generation-files"; then + schema_guard_passed=$((schema_guard_passed + 1)) +else + fail_case "private guard rejected accepted growing private members" +fi cp "$schema_generation_files" "$schema_test_tmp/invalid-generation-files" printf '%s\n' "core/v1/generations/$schema_generation/contracts.jq" >> \ "$schema_test_tmp/invalid-generation-files" @@ -493,6 +574,14 @@ if ! guard_paths_ok "$schema_test_tmp/invalid-generation-files"; then else fail_case "private guard accepted a synthetic public root" fi +cp "$schema_generation_files" "$schema_test_tmp/unknown-generation-files" +printf '%s\n' "core/v1/generations/$schema_generation/modules/unknown.jq" >> \ + "$schema_test_tmp/unknown-generation-files" +if ! guard_paths_ok "$schema_test_tmp/unknown-generation-files"; then + schema_guard_passed=$((schema_guard_passed + 1)) +else + fail_case "private guard accepted a synthetic unknown member" +fi mark_rule portable-core-schema.private-activation-guard schema_manifest_paths="core/v1/generation-registry.json @@ -508,10 +597,18 @@ while IFS= read -r schema_required_path; do fi done <<< "$schema_manifest_paths" -if [ "$(awk -F '\t' 'NR > 1 && $1 == "review" {count++} END {print count + 0}' "$schema_ledger")" -ne 8 ]; then +schema_mapping_digest="$("$schema_jq" -r '.metadata.schema_mapping_sha256' "$schema_fixture")" +if [ "$(sha256_path "$schema_ledger")" != "$schema_mapping_digest" ]; then + fail_case "schema ledger mapping digest" +fi +schema_review_total="$(awk -F '\t' 'NR > 1 && $1 == "review" {count++} END {print count + 0}' "$schema_ledger")" +schema_legacy_total="$(awk -F '\t' 'NR > 1 && $1 == "legacy" {count++} END {print count + 0}' "$schema_ledger")" +schema_expected_review="$("$schema_jq" -r '.metadata.review_rows' "$schema_fixture")" +schema_expected_legacy="$("$schema_jq" -r '.metadata.legacy_rows' "$schema_fixture")" +if [ "$schema_review_total" -ne "$schema_expected_review" ]; then fail_case "review ledger row count" fi -if [ "$(awk -F '\t' 'NR > 1 && $1 == "legacy" {count++} END {print count + 0}' "$schema_ledger")" -ne 44 ]; then +if [ "$schema_legacy_total" -ne "$schema_expected_legacy" ]; then fail_case "legacy ledger row count" fi if [ "$(tail -n +2 "$schema_ledger" | cut -f2 | sort -u | wc -l | tr -d ' ')" -ne 52 ]; then @@ -523,16 +620,39 @@ LC_ALL=C sort "$schema_seen_rules" > "$schema_test_tmp/seen-rules.sorted" if ! cmp -s "$schema_expected_rules" "$schema_test_tmp/seen-rules.sorted"; then fail_case "owned-rule inventory does not match executed rule proof" fi +schema_expected_tests="$schema_test_tmp/expected-tests" +tail -n +2 "$schema_ledger" | cut -f5 | LC_ALL=C sort -u > "$schema_expected_tests" +LC_ALL=C sort "$schema_seen_tests" > "$schema_test_tmp/seen-tests.sorted" +if ! cmp -s "$schema_expected_tests" "$schema_test_tmp/seen-tests.sorted"; then + fail_case "ledger test IDs do not match the executed stable-ID inventory" +fi while IFS=$'\t' read -r schema_source schema_row schema_disposition schema_rule schema_test_id; do [ "$schema_source" != "source" ] || continue if ! grep -Fqx "$schema_rule" "$schema_expected_rules" || - [[ ! "$schema_test_id" =~ ^portable-core-schema\.test\. ]] || + ! grep -Fqx "$schema_test_id" "$schema_test_tmp/seen-tests.sorted" || [[ ! "$schema_row" =~ ^(review-r[0-3]-f[0-9]{2}|legacy-test-[0-9]{3})$ ]] || [[ ! "$schema_disposition" =~ ^(ported|replaced-by)$ ]]; then fail_case "invalid ledger mapping row: $schema_row" fi done < "$schema_ledger" +schema_review_accounted="$(awk -F '\t' ' + NR == FNR {executed[$1] = 1; next} + FNR > 1 && $1 == "review" && ($5 in executed) {count++} + END {print count + 0} +' "$schema_test_tmp/seen-tests.sorted" "$schema_ledger")" +schema_legacy_accounted="$(awk -F '\t' ' + NR == FNR {executed[$1] = 1; next} + FNR > 1 && $1 == "legacy" && ($5 in executed) {count++} + END {print count + 0} +' "$schema_test_tmp/seen-tests.sorted" "$schema_ledger")" +if [ "$schema_review_accounted" -ne "$schema_review_total" ]; then + fail_case "unexecuted review-ledger test mapping" +fi +if [ "$schema_legacy_accounted" -ne "$schema_legacy_total" ]; then + fail_case "unexecuted legacy-ledger test mapping" +fi + schema_owned_total="$(wc -l < "$schema_expected_rules" | tr -d ' ')" schema_owned_passed="$schema_owned_total" if [ "$schema_failures" -ne 0 ]; then @@ -544,8 +664,8 @@ printf 'direct cases: %s/%s\n' "$schema_direct_passed" "$schema_direct_total" printf 'private route probes: %s/%s\n' "$schema_route_passed" "$schema_route_total" printf 'registry cases: %s/%s\n' "$schema_registry_passed" "$schema_registry_total" printf 'activation guard cases: %s/%s\n' "$schema_guard_passed" "$schema_guard_total" -printf 'review findings accounted for: 8/8\n' -printf 'legacy assertions accounted for: 44/44\n' +printf 'review findings accounted for: %s/%s\n' "$schema_review_accounted" "$schema_review_total" +printf 'legacy assertions accounted for: %s/%s\n' "$schema_legacy_accounted" "$schema_legacy_total" printf 'failures: %s\n' "$schema_failures" [ "$schema_failures" -eq 0 ] From eb93e13aac3f07b562efc0aaa283277e130894cd Mon Sep 17 00:00:00 2001 From: ci Date: Sat, 29 Aug 2026 21:29:46 -0400 Subject: [PATCH 3/6] test: scope private generation mentions --- scripts/test/portable-core-schema.test.sh | 35 ++++++++++++++++++----- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/scripts/test/portable-core-schema.test.sh b/scripts/test/portable-core-schema.test.sh index 84f7dfa..4443c4b 100755 --- a/scripts/test/portable-core-schema.test.sh +++ b/scripts/test/portable-core-schema.test.sh @@ -473,7 +473,7 @@ guard_paths_ok() { done < "$paths_file" } -schema_guard_total=10 +schema_guard_total=11 schema_generation_files="$schema_test_tmp/generation-files" find "$schema_root/core/v1/generations/$schema_generation" -type f -print | \ sed "s#^$schema_root/##" | LC_ALL=C sort > "$schema_generation_files" @@ -519,14 +519,35 @@ else fail_case "ambient module path changed the fixed schema import" fi schema_live_hits="$schema_test_tmp/live-hits" -if grep -RIl --exclude='portable-core-*' -- "$schema_generation" \ - "$schema_root/manager" "$schema_root/routines" "$schema_root/templates" \ - "$schema_root/reviewer" "$schema_root/config" "$schema_root/scripts" \ - "$schema_root/README.md" "$schema_root/QUICKSTART.md" "$schema_root/RESTORE.md" \ - > "$schema_live_hits" 2>/dev/null; then - fail_case "generation ID appears in a live or public caller" +private_mentions_ok() { + local mentions_file="$1" + local mention_path + while IFS= read -r mention_path; do + mention_path="${mention_path#"$schema_root/"}" + case "$mention_path" in + scripts/test/portable-core-*) ;; + *) return 1 ;; + esac + done < "$mentions_file" +} + +grep -RIl -- "$schema_generation" \ + "$schema_root/manager" "$schema_root/routines" "$schema_root/templates" \ + "$schema_root/reviewer" "$schema_root/config" "$schema_root/scripts" \ + "$schema_root/README.md" "$schema_root/QUICKSTART.md" "$schema_root/RESTORE.md" \ + > "$schema_live_hits" 2>/dev/null || true +if private_mentions_ok "$schema_live_hits"; then + schema_guard_passed=$((schema_guard_passed + 1)) else + fail_case "generation ID appears in a live, public, or non-private-test file" +fi +cp "$schema_live_hits" "$schema_test_tmp/invalid-live-hits" +printf '%s\n' "$schema_root/scripts/portable-core-loader.sh" >> \ + "$schema_test_tmp/invalid-live-hits" +if ! private_mentions_ok "$schema_test_tmp/invalid-live-hits"; then schema_guard_passed=$((schema_guard_passed + 1)) +else + fail_case "private mention guard accepted a synthetic live prefixed caller" fi schema_non_test_loaders="$schema_test_tmp/non-test-loaders" grep -RIl -- 'import "schema" as schema' "$schema_root" \ From ae877c417ff6220f3c89ef60ca88d14b51753f6f Mon Sep 17 00:00:00 2001 From: ci Date: Sat, 29 Aug 2026 21:40:39 -0400 Subject: [PATCH 4/6] test: close portable activation scans --- scripts/test/portable-core-schema.test.sh | 142 ++++++++++++++-------- 1 file changed, 92 insertions(+), 50 deletions(-) diff --git a/scripts/test/portable-core-schema.test.sh b/scripts/test/portable-core-schema.test.sh index 4443c4b..7f30c6b 100755 --- a/scripts/test/portable-core-schema.test.sh +++ b/scripts/test/portable-core-schema.test.sh @@ -457,23 +457,27 @@ fi mark_rule portable-core-schema.generation-registry-entry mark_rule portable-core-schema.generation-registry-prefix +private_generation_path_ok() { + case "$1" in + "core/v1/generations/$schema_generation/core-ingress.sh"|\ + "core/v1/generations/$schema_generation/modules/schema.jq"|\ + "core/v1/generations/$schema_generation/modules/profile_graph.jq"|\ + "core/v1/generations/$schema_generation/modules/stage_request.jq"|\ + "core/v1/generations/$schema_generation/modules/result_facts.jq"|\ + "core/v1/generations/$schema_generation/modules/result_truth.jq") return 0 ;; + *) return 1 ;; + esac +} + guard_paths_ok() { local paths_file="$1" local generation_path while IFS= read -r generation_path; do - case "$generation_path" in - "core/v1/generations/$schema_generation/core-ingress.sh"|\ - "core/v1/generations/$schema_generation/modules/schema.jq"|\ - "core/v1/generations/$schema_generation/modules/profile_graph.jq"|\ - "core/v1/generations/$schema_generation/modules/stage_request.jq"|\ - "core/v1/generations/$schema_generation/modules/result_facts.jq"|\ - "core/v1/generations/$schema_generation/modules/result_truth.jq") ;; - *) return 1 ;; - esac + private_generation_path_ok "$generation_path" || return 1 done < "$paths_file" } -schema_guard_total=11 +schema_guard_total=17 schema_generation_files="$schema_test_tmp/generation-files" find "$schema_root/core/v1/generations/$schema_generation" -type f -print | \ sed "s#^$schema_root/##" | LC_ALL=C sort > "$schema_generation_files" @@ -518,56 +522,94 @@ if [ "$schema_loaded_identity" = "core.contracts.v1" ]; then else fail_case "ambient module path changed the fixed schema import" fi -schema_live_hits="$schema_test_tmp/live-hits" -private_mentions_ok() { - local mentions_file="$1" - local mention_path - while IFS= read -r mention_path; do - mention_path="${mention_path#"$schema_root/"}" - case "$mention_path" in - scripts/test/portable-core-*) ;; - *) return 1 ;; +tracked_activation_paths_ok() { + local paths_file="$1" + local activation_path + local test_path + while IFS= read -r activation_path; do + case "$activation_path" in + ci/required-files.txt|core/v1/generation-registry.json) ;; + scripts/test/portable-core-*) + test_path="${activation_path#scripts/test/}" + case "$test_path" in */*) return 1 ;; esac + ;; + *) private_generation_path_ok "$activation_path" || return 1 ;; esac - done < "$mentions_file" + done < "$paths_file" } -grep -RIl -- "$schema_generation" \ - "$schema_root/manager" "$schema_root/routines" "$schema_root/templates" \ - "$schema_root/reviewer" "$schema_root/config" "$schema_root/scripts" \ - "$schema_root/README.md" "$schema_root/QUICKSTART.md" "$schema_root/RESTORE.md" \ - > "$schema_live_hits" 2>/dev/null || true -if private_mentions_ok "$schema_live_hits"; then +schema_live_hits="$schema_test_tmp/live-hits" +schema_import_hits="$schema_test_tmp/import-hits" +: > "$schema_live_hits" +: > "$schema_import_hits" +schema_import_pattern='import[[:space:]]+"schema"[[:space:]]+as[[:space:]]+' +while IFS= read -r -d '' schema_tracked_path; do + if ! schema_scan_result="$( + git -C "$schema_root" show ":$schema_tracked_path" 2>/dev/null | + "$schema_jq" -Rsr \ + --arg generation "$schema_generation" \ + --arg import_pattern "$schema_import_pattern" \ + '[contains($generation),test($import_pattern)] | @tsv' + )"; then + fail_case "unable to scan tracked path: $schema_tracked_path" + continue + fi + IFS=$'\t' read -r schema_has_generation schema_has_import <<< "$schema_scan_result" + if [ "$schema_has_generation" = true ]; then + printf '%s\n' "$schema_tracked_path" >> "$schema_live_hits" + fi + if [ "$schema_has_import" = true ]; then + printf '%s\n' "$schema_tracked_path" >> "$schema_import_hits" + fi +done < <(git -C "$schema_root" ls-files -z) + +if tracked_activation_paths_ok "$schema_live_hits"; then schema_guard_passed=$((schema_guard_passed + 1)) else - fail_case "generation ID appears in a live, public, or non-private-test file" + fail_case "generation ID appears outside the closed tracked-path allowlist" fi -cp "$schema_live_hits" "$schema_test_tmp/invalid-live-hits" -printf '%s\n' "$schema_root/scripts/portable-core-loader.sh" >> \ - "$schema_test_tmp/invalid-live-hits" -if ! private_mentions_ok "$schema_test_tmp/invalid-live-hits"; then +if tracked_activation_paths_ok "$schema_import_hits"; then schema_guard_passed=$((schema_guard_passed + 1)) else - fail_case "private mention guard accepted a synthetic live prefixed caller" -fi -schema_non_test_loaders="$schema_test_tmp/non-test-loaders" -grep -RIl -- 'import "schema" as schema' "$schema_root" \ - > "$schema_non_test_loaders" 2>/dev/null || true -schema_loaders_ok=true -while IFS= read -r schema_loader; do - schema_loader="${schema_loader#"$schema_root/"}" - case "$schema_loader" in - "core/v1/generations/$schema_generation/modules/profile_graph.jq"|\ - "core/v1/generations/$schema_generation/modules/stage_request.jq"|\ - "core/v1/generations/$schema_generation/modules/result_facts.jq"|\ - "core/v1/generations/$schema_generation/modules/result_truth.jq"|\ - scripts/test/portable-core-*) ;; - *) schema_loaders_ok=false ;; - esac -done < "$schema_non_test_loaders" -if [ "$schema_loaders_ok" = true ]; then + fail_case "schema import appears outside the closed tracked-path allowlist" +fi + +for schema_invalid_path in \ + .claude/hooks/portable-core-loader.sh \ + core/other/loader.jq \ + docs/portable-core.md \ + scripts/portable-core-loader.sh; do + printf '%s\n' "$schema_invalid_path" > "$schema_test_tmp/invalid-tracked-path" + if ! tracked_activation_paths_ok "$schema_test_tmp/invalid-tracked-path"; then + schema_guard_passed=$((schema_guard_passed + 1)) + else + fail_case "tracked-path guard accepted: $schema_invalid_path" + fi +done + +schema_alias_import="$schema_test_tmp/alias-import.jq" +schema_whitespace_import="$schema_test_tmp/whitespace-import.jq" +schema_other_import="$schema_test_tmp/other-import.jq" +printf '%s\n' 'import "schema" as s;' > "$schema_alias_import" +printf 'import\t"schema"\n as\tother ;\n' > "$schema_whitespace_import" +printf '%s\n' 'import "profile_graph" as schema;' > "$schema_other_import" +if "$schema_jq" -Rse --arg pattern "$schema_import_pattern" \ + 'test($pattern)' < "$schema_alias_import" >/dev/null; then + schema_guard_passed=$((schema_guard_passed + 1)) +else + fail_case "alternate schema import alias was not detected" +fi +if "$schema_jq" -Rse --arg pattern "$schema_import_pattern" \ + 'test($pattern)' < "$schema_whitespace_import" >/dev/null; then + schema_guard_passed=$((schema_guard_passed + 1)) +else + fail_case "legal-whitespace schema import was not detected" +fi +if ! "$schema_jq" -Rse --arg pattern "$schema_import_pattern" \ + 'test($pattern)' < "$schema_other_import" >/dev/null; then schema_guard_passed=$((schema_guard_passed + 1)) else - fail_case "a public, unknown, or non-test caller loads the incomplete schema" + fail_case "non-schema import was classified as schema import" fi if guard_paths_ok "$schema_generation_files"; then schema_guard_passed=$((schema_guard_passed + 1)) From 350483f53e88d60f9030e6d7ceea14023bf88d2a Mon Sep 17 00:00:00 2001 From: ci Date: Sat, 29 Aug 2026 21:52:00 -0400 Subject: [PATCH 5/6] test: enforce schema load and number boundaries --- .../modules/schema.jq | 8 +- scripts/test/portable-core-schema.test.sh | 95 ++++++++++++++----- 2 files changed, 76 insertions(+), 27 deletions(-) diff --git a/core/v1/generations/g-14b7ad8ce54c3b8c585ff92063d71551ffc7394cc2294d0297bc7d2b8da2c386/modules/schema.jq b/core/v1/generations/g-14b7ad8ce54c3b8c585ff92063d71551ffc7394cc2294d0297bc7d2b8da2c386/modules/schema.jq index 3f6fee3..32ac4bc 100644 --- a/core/v1/generations/g-14b7ad8ce54c3b8c585ff92063d71551ffc7394cc2294d0297bc7d2b8da2c386/modules/schema.jq +++ b/core/v1/generations/g-14b7ad8ce54c3b8c585ff92063d71551ffc7394cc2294d0297bc7d2b8da2c386/modules/schema.jq @@ -141,7 +141,13 @@ def present_ok(value_ok): (.value | value_ok)); def id_ok: type == "string" and test("^[a-z0-9][a-z0-9._:-]{0,127}$"); -def int_ok: type == "number" and . == floor and . >= 0 and . <= 2147483647; +# Schema receives only values accepted by the raw canonical-byte gate; jq 1.6 preserves -0 here. +def int_ok: + type == "number" and + . == floor and + . >= 0 and + . <= 2147483647 and + tostring != "-0"; def sha256_ok: type == "string" and test("^[0-9a-f]{64}$"); def version_ok: id_ok; def short_text_ok: type == "string" and utf8bytelength >= 1 and utf8bytelength <= 1024; diff --git a/scripts/test/portable-core-schema.test.sh b/scripts/test/portable-core-schema.test.sh index 7f30c6b..7970351 100755 --- a/scripts/test/portable-core-schema.test.sh +++ b/scripts/test/portable-core-schema.test.sh @@ -86,6 +86,8 @@ schema_registry_total=0 schema_registry_passed=0 schema_guard_total=0 schema_guard_passed=0 +schema_numeric_total=0 +schema_numeric_passed=0 schema_seen_rules="$schema_test_tmp/seen-rules" schema_seen_tests="$schema_test_tmp/seen-tests" : > "$schema_seen_rules" @@ -123,6 +125,30 @@ expect_false() { expect_jq "$1" false "$2" } +expect_canonical_raw() { + local case_id="$1" + local expected="$2" + local raw_bytes="$3" + local raw_input + local canonical_output + local actual=false + schema_numeric_total=$((schema_numeric_total + 1)) + raw_input="$schema_test_tmp/numeric-$schema_numeric_total.input" + canonical_output="$schema_test_tmp/numeric-$schema_numeric_total.canonical" + printf '%s' "$raw_bytes" > "$raw_input" + if "$schema_jq" -s -S -c \ + 'if length == 1 then .[0] else error("root-count") end' \ + < "$raw_input" > "$canonical_output" 2>/dev/null && + cmp -s "$raw_input" "$canonical_output"; then + actual=true + fi + if [ "$actual" = "$expected" ]; then + schema_numeric_passed=$((schema_numeric_passed + 1)) + else + fail_case "$case_id expected canonical=$expected, got canonical=$actual" + fi +} + mark_rule() { printf '%s\n' "$1" >> "$schema_seen_rules" } @@ -166,6 +192,17 @@ expect_true int-boundaries '(0 | schema::int_ok) and (2147483647 | schema::int_o expect_false int-negative '-1 | schema::int_ok' expect_false int-one-over '2147483648 | schema::int_ok' expect_false int-float '1.5 | schema::int_ok' +expect_false int-negative-zero '-0 | schema::int_ok' +expect_canonical_raw raw-integer true $'1\n' +expect_canonical_raw raw-integral-float false $'1.0\n' +expect_canonical_raw raw-integral-exponent false $'1e0\n' +expect_canonical_raw raw-fraction true $'1.5\n' +expect_canonical_raw raw-negative-zero true $'-0\n' +expect_canonical_raw raw-integer-field-float false $'{"attempt":1.0}\n' +expect_canonical_raw raw-schema-version-float false \ + $'{"body":{},"id":"doc.example","kind":"profile","schema_version":1.0}\n' +expect_canonical_raw raw-schema-version-integer true \ + $'{"body":{},"id":"doc.example","kind":"profile","schema_version":1}\n' mark_rule portable-core-schema.integer-domain mark_test portable-core-schema.test.legacy-075-float-value mark_test portable-core-schema.test.legacy-077-negative-integer @@ -477,7 +514,7 @@ guard_paths_ok() { done < "$paths_file" } -schema_guard_total=17 +schema_guard_total=25 schema_generation_files="$schema_test_tmp/generation-files" find "$schema_root/core/v1/generations/$schema_generation" -type f -print | \ sed "s#^$schema_root/##" | LC_ALL=C sort > "$schema_generation_files" @@ -542,7 +579,7 @@ schema_live_hits="$schema_test_tmp/live-hits" schema_import_hits="$schema_test_tmp/import-hits" : > "$schema_live_hits" : > "$schema_import_hits" -schema_import_pattern='import[[:space:]]+"schema"[[:space:]]+as[[:space:]]+' +schema_import_pattern='(^|[^A-Za-z0-9_])(import|include)[[:space:]]*"schema"' while IFS= read -r -d '' schema_tracked_path; do if ! schema_scan_result="$( git -C "$schema_root" show ":$schema_tracked_path" 2>/dev/null | @@ -587,30 +624,35 @@ for schema_invalid_path in \ fi done -schema_alias_import="$schema_test_tmp/alias-import.jq" -schema_whitespace_import="$schema_test_tmp/whitespace-import.jq" -schema_other_import="$schema_test_tmp/other-import.jq" -printf '%s\n' 'import "schema" as s;' > "$schema_alias_import" -printf 'import\t"schema"\n as\tother ;\n' > "$schema_whitespace_import" -printf '%s\n' 'import "profile_graph" as schema;' > "$schema_other_import" -if "$schema_jq" -Rse --arg pattern "$schema_import_pattern" \ - 'test($pattern)' < "$schema_alias_import" >/dev/null; then - schema_guard_passed=$((schema_guard_passed + 1)) -else - fail_case "alternate schema import alias was not detected" -fi -if "$schema_jq" -Rse --arg pattern "$schema_import_pattern" \ - 'test($pattern)' < "$schema_whitespace_import" >/dev/null; then - schema_guard_passed=$((schema_guard_passed + 1)) -else - fail_case "legal-whitespace schema import was not detected" -fi -if ! "$schema_jq" -Rse --arg pattern "$schema_import_pattern" \ - 'test($pattern)' < "$schema_other_import" >/dev/null; then - schema_guard_passed=$((schema_guard_passed + 1)) -else - fail_case "non-schema import was classified as schema import" -fi +schema_load_case() { + local case_id="$1" + local expected="$2" + local source_text="$3" + local source_file="$schema_test_tmp/$case_id.jq" + local actual=false + printf '%s' "$source_text" > "$source_file" + if "$schema_jq" -Rse --arg pattern "$schema_import_pattern" \ + 'test($pattern)' < "$source_file" >/dev/null; then + actual=true + fi + if [ "$actual" = "$expected" ]; then + schema_guard_passed=$((schema_guard_passed + 1)) + else + fail_case "$case_id expected schema-load=$expected, got schema-load=$actual" + fi +} + +schema_load_case import-spaced true $'import "schema" as s;\n' +schema_load_case import-compact true $'import"schema"as s;\n' +schema_load_case import-arbitrary-alias true $'import "schema" as any_alias_42;\n' +schema_load_case import-newlines true $'import\n "schema"\n as\n another_alias;\n' +schema_load_case import-metadata true $'import"schema"as s {search:"."};\n' +schema_load_case include-spaced true $'include "schema";\n' +schema_load_case include-compact true $'include"schema";\n' +schema_load_case include-newlines true $'include\n "schema" ;\n' +schema_load_case other-module false $'import "profile_graph" as schema;\n' +schema_load_case longer-module false $'include "schema-extra";\n' +schema_load_case keyword-substring false $'myimport"schema"as s;\n' if guard_paths_ok "$schema_generation_files"; then schema_guard_passed=$((schema_guard_passed + 1)) else @@ -727,6 +769,7 @@ printf 'direct cases: %s/%s\n' "$schema_direct_passed" "$schema_direct_total" printf 'private route probes: %s/%s\n' "$schema_route_passed" "$schema_route_total" printf 'registry cases: %s/%s\n' "$schema_registry_passed" "$schema_registry_total" printf 'activation guard cases: %s/%s\n' "$schema_guard_passed" "$schema_guard_total" +printf 'numeric boundary cases: %s/%s\n' "$schema_numeric_passed" "$schema_numeric_total" printf 'review findings accounted for: %s/%s\n' "$schema_review_accounted" "$schema_review_total" printf 'legacy assertions accounted for: %s/%s\n' "$schema_legacy_accounted" "$schema_legacy_total" printf 'failures: %s\n' "$schema_failures" From 87daa48d08e6d9ac21a356bd9c4ab0350da91e86 Mon Sep 17 00:00:00 2001 From: ci Date: Sat, 29 Aug 2026 22:05:44 -0400 Subject: [PATCH 6/6] fix: enforce absolute schema boundaries --- .../modules/schema.jq | 24 ++-- scripts/test/portable-core-schema.test.sh | 113 ++++++++++++++++-- 2 files changed, 115 insertions(+), 22 deletions(-) diff --git a/core/v1/generations/g-14b7ad8ce54c3b8c585ff92063d71551ffc7394cc2294d0297bc7d2b8da2c386/modules/schema.jq b/core/v1/generations/g-14b7ad8ce54c3b8c585ff92063d71551ffc7394cc2294d0297bc7d2b8da2c386/modules/schema.jq index 32ac4bc..fd3924d 100644 --- a/core/v1/generations/g-14b7ad8ce54c3b8c585ff92063d71551ffc7394cc2294d0297bc7d2b8da2c386/modules/schema.jq +++ b/core/v1/generations/g-14b7ad8ce54c3b8c585ff92063d71551ffc7394cc2294d0297bc7d2b8da2c386/modules/schema.jq @@ -140,7 +140,7 @@ def present_ok(value_ok): .state == "present" and (.value | value_ok)); -def id_ok: type == "string" and test("^[a-z0-9][a-z0-9._:-]{0,127}$"); +def id_ok: type == "string" and test("\\A[a-z0-9][a-z0-9._:-]{0,127}\\z"); # Schema receives only values accepted by the raw canonical-byte gate; jq 1.6 preserves -0 here. def int_ok: type == "number" and @@ -148,25 +148,27 @@ def int_ok: . >= 0 and . <= 2147483647 and tostring != "-0"; -def sha256_ok: type == "string" and test("^[0-9a-f]{64}$"); +def sha256_ok: type == "string" and test("\\A[0-9a-f]{64}\\z"); def version_ok: id_ok; def short_text_ok: type == "string" and utf8bytelength >= 1 and utf8bytelength <= 1024; def media_type_ok: type == "string" and length <= 127 and - test("^[a-z0-9][a-z0-9!#$&^_.+-]*/[a-z0-9][a-z0-9!#$&^_.+-]*$"); + test("\\A[a-z0-9][a-z0-9!#$&^_.+-]*/[a-z0-9][a-z0-9!#$&^_.+-]*\\z"); def patch_media_type_ok: . == "text/x-diff"; -def git_oid_ok: type == "string" and (test("^[0-9a-f]{40}$") or test("^[0-9a-f]{64}$")); +def git_oid_ok: + type == "string" and + (test("\\A[0-9a-f]{40}\\z") or test("\\A[0-9a-f]{64}\\z")); def reverse_dns_ok: type == "string" and (split(".") as $labels | ($labels | length) >= 2 and - all($labels[]; test("^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$"))); + all($labels[]; test("\\A[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\z"))); def time_ok: type == "string" and - test("^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$") and - (capture("^(?[0-9]{4})-(?[0-9]{2})-(?[0-9]{2})T(?[0-9]{2}):(?[0-9]{2}):(?[0-9]{2})Z$") as $parts | + test("\\A[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z\\z") and + (capture("\\A(?[0-9]{4})-(?[0-9]{2})-(?[0-9]{2})T(?[0-9]{2}):(?[0-9]{2}):(?[0-9]{2})Z\\z") as $parts | ($parts.year | tonumber) as $year | ($parts.month | tonumber) as $month | ($parts.day | tonumber) as $day | @@ -248,8 +250,8 @@ def git_revision_ref_ok: (.repository_id | id_ok) and (.hash_algorithm == "sha1" or .hash_algorithm == "sha256") and (if .hash_algorithm == "sha1" - then (.commit_id | type == "string" and test("^[0-9a-f]{40}$")) - else (.commit_id | type == "string" and test("^[0-9a-f]{64}$")) + then (.commit_id | type == "string" and test("\\A[0-9a-f]{40}\\z")) + else (.commit_id | type == "string" and test("\\A[0-9a-f]{64}\\z")) end); def git_location_ok: @@ -264,8 +266,8 @@ def git_object_ref_ok: (.location | git_location_ok) and (.object_type == "blob" or .object_type == "tree") and (if .revision.hash_algorithm == "sha1" - then (.object_id | type == "string" and test("^[0-9a-f]{40}$")) - else (.object_id | type == "string" and test("^[0-9a-f]{64}$")) + then (.object_id | type == "string" and test("\\A[0-9a-f]{40}\\z")) + else (.object_id | type == "string" and test("\\A[0-9a-f]{64}\\z")) end) and (if .location.kind == "root" then .object_type == "tree" else true end) and (if .object_type == "tree" diff --git a/scripts/test/portable-core-schema.test.sh b/scripts/test/portable-core-schema.test.sh index 7970351..89cda4e 100755 --- a/scripts/test/portable-core-schema.test.sh +++ b/scripts/test/portable-core-schema.test.sh @@ -186,6 +186,7 @@ mark_rule portable-core-schema.present-union expect_true id-boundary '(("a" * 128) | schema::id_ok) and ("a" | schema::id_ok)' expect_false id-one-over '("a" * 129) | schema::id_ok' expect_false id-uppercase '"Bad" | schema::id_ok' +expect_false id-trailing-newline '"id.example\n" | schema::id_ok' mark_rule portable-core-schema.id expect_true int-boundaries '(0 | schema::int_ok) and (2147483647 | schema::int_ok)' @@ -210,6 +211,7 @@ mark_test portable-core-schema.test.legacy-079-integer-over-2147483647 expect_true sha-valid '$f.valid.sha256 | schema::sha256_ok' expect_false sha-invalid '"Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" | schema::sha256_ok' +expect_false sha-trailing-newline '($f.valid.sha256 + "\n") | schema::sha256_ok' mark_rule portable-core-schema.sha256 expect_true short-text-boundary '(("a" * 1024) | schema::short_text_ok) and ("é" | schema::short_text_ok)' @@ -223,6 +225,7 @@ expect_false time-malformed '"2024-2-29T00:00:00Z" | schema::time_ok' expect_false time-fields '"2024-13-01T24:60:60Z" | schema::time_ok' expect_false time-calendar '"2024-04-31T00:00:00Z" | schema::time_ok' expect_false time-non-leap '"2025-02-29T00:00:00Z" | schema::time_ok' +expect_false time-trailing-newline '"2024-02-29T23:59:59Z\n" | schema::time_ok' mark_rule portable-core-schema.utc-timestamp-valid-instant mark_rule portable-core-schema.time mark_test portable-core-schema.test.utc-timestamp-field-range-rejected @@ -236,6 +239,7 @@ mark_test portable-core-schema.test.legacy-167-requested-at-names-feb-29-in-a-re expect_true media-boundary '("a/" + ("b" * 125)) | schema::media_type_ok' expect_false media-one-over '("a/" + ("b" * 126)) | schema::media_type_ok' expect_false media-uppercase '"Application/json" | schema::media_type_ok' +expect_false media-trailing-newline '"application/json\n" | schema::media_type_ok' mark_rule portable-core-schema.media-type expect_true patch-media-valid '"text/x-diff" | schema::patch_media_type_ok' @@ -244,11 +248,14 @@ mark_rule portable-core-schema.patch-media-type expect_true git-oid-boundaries '("a" * 40 | schema::git_oid_ok) and ("b" * 64 | schema::git_oid_ok)' expect_false git-oid-invalid '("a" * 41) | schema::git_oid_ok' +expect_false git-oid-trailing-newline '(("a" * 40) + "\n") | schema::git_oid_ok' mark_rule portable-core-schema.git-oid expect_true reverse-dns-boundaries '("a.b" | schema::reverse_dns_ok) and (("a" * 63) + ".b" | schema::reverse_dns_ok)' expect_false reverse-dns-label-over '(("a" * 64) + ".b") | schema::reverse_dns_ok' expect_false reverse-dns-edge-hyphen '"-a.example" | schema::reverse_dns_ok' +expect_false reverse-dns-trailing-newline '"example.test\n" | schema::reverse_dns_ok' +expect_false reverse-dns-label-newline '"example\n.test" | schema::reverse_dns_ok' mark_rule portable-core-schema.reverse-dns expect_true repo-path-unicode '"src/合法.json" | schema::repo_path_ok' @@ -317,6 +324,7 @@ mark_rule portable-core-schema.document-kind expect_true envelope-five-kinds 'all($f.valid.envelopes[]; . as $doc | $doc | schema::envelope_ok($doc.kind))' expect_false envelope-unknown '$f.valid.envelopes[0] + {kind:"unknown"} | schema::document_envelope_ok' +expect_false envelope-kind-trailing-newline '$f.valid.envelopes[0] + {kind:"adapter_manifest\n"} | schema::document_envelope_ok' mark_rule portable-core-schema.envelope-kind mark_test portable-core-schema.test.legacy-093-unknown-document-kind expect_false envelope-version '$f.valid.envelopes[0] + {schema_version:2} | schema::document_envelope_ok' @@ -324,58 +332,79 @@ mark_rule portable-core-schema.envelope-version mark_test portable-core-schema.test.legacy-095-wrong-schema-version expect_false envelope-extra '$f.valid.envelopes[0] + {extra:true} | schema::document_envelope_ok' expect_false envelope-body-type '$f.valid.envelopes[0] + {body:[]} | schema::document_envelope_ok' +expect_false envelope-id-trailing-newline '$f.valid.envelopes[0] + {id:"manifest.example\n"} | schema::document_envelope_ok' mark_rule portable-core-schema.envelope-exact-fields expect_true document-ref-valid '$f.valid.document_ref | schema::document_ref_kind_ok("stage_result")' expect_false document-ref-kind '$f.valid.document_ref + {kind:"unknown"} | schema::document_ref_ok' +expect_false document-ref-kind-trailing-newline '$f.valid.document_ref + {kind:"stage_result\n"} | schema::document_ref_ok' +expect_false document-ref-id-trailing-newline '$f.valid.document_ref + {id:"result.example\n"} | schema::document_ref_ok' +expect_false document-ref-sha-trailing-newline '$f.valid.document_ref + {sha256:($f.valid.document_ref.sha256 + "\n")} | schema::document_ref_ok' mark_rule portable-core-schema.document-ref-kind mark_test portable-core-schema.test.legacy-161-named-input-document-ref-with-an-unknown-document-kind expect_true git-revision-valid '$f.valid.git_revision_sha1 | schema::git_revision_ref_ok' expect_false git-revision-algorithm '$f.valid.git_revision_sha1 + {hash_algorithm:"sha256"} | schema::git_revision_ref_ok' +expect_false git-revision-repository-newline '$f.valid.git_revision_sha1 + {repository_id:"repo.example\n"} | schema::git_revision_ref_ok' +expect_false git-revision-algorithm-newline '$f.valid.git_revision_sha1 + {hash_algorithm:"sha1\n"} | schema::git_revision_ref_ok' +expect_false git-revision-commit-newline '$f.valid.git_revision_sha1 + {commit_id:($f.valid.git_revision_sha1.commit_id + "\n")} | schema::git_revision_ref_ok' mark_rule portable-core-schema.git-revision-ref expect_true git-location-variants '({kind:"root"} | schema::git_location_ok) and ({kind:"path",value:"src/main"} | schema::git_location_ok)' expect_false git-location-invalid '{kind:"path",value:"../main"} | schema::git_location_ok' +expect_false git-location-newline '{kind:"path",value:"src/main\n"} | schema::git_location_ok' mark_rule portable-core-schema.git-location expect_true git-object-variants '($f.valid.git_blob | schema::git_object_ref_ok) and ($f.valid.git_tree | schema::git_object_ref_ok)' expect_false git-object-mode '$f.valid.git_blob + {mode:"120000"} | schema::git_object_ref_ok' expect_false git-object-root-blob '$f.valid.git_blob + {location:{kind:"root"}} | schema::git_object_ref_ok' +expect_false git-object-id-newline '$f.valid.git_blob + {object_id:($f.valid.git_blob.object_id + "\n")} | schema::git_object_ref_ok' +expect_false git-object-mode-newline '$f.valid.git_blob + {mode:"100644\n"} | schema::git_object_ref_ok' mark_rule portable-core-schema.git-object-ref expect_true content-ref-valid '$f.valid.content_ref | schema::content_ref_ok' expect_false content-ref-invalid '$f.valid.content_ref + {media_type:"Application/json"} | schema::content_ref_ok' expect_false content-ref-url-id '$f.valid.content_ref + {content_id:"https:payload"} | schema::content_ref_ok' expect_false content-ref-path-id '$f.valid.content_ref + {content_id:"path/value"} | schema::content_ref_ok' +expect_false content-ref-id-newline '$f.valid.content_ref + {content_id:"content.example\n"} | schema::content_ref_ok' +expect_false content-ref-media-newline '$f.valid.content_ref + {media_type:"application/json\n"} | schema::content_ref_ok' +expect_false content-ref-sha-newline '$f.valid.content_ref + {sha256:($f.valid.content_ref.sha256 + "\n")} | schema::content_ref_ok' mark_rule portable-core-schema.content-ref expect_true artifact-ref-variants '({type:"git-object",value:$f.valid.git_blob} | schema::artifact_ref_ok) and ({type:"content",value:$f.valid.content_ref} | schema::artifact_ref_ok)' expect_false artifact-ref-invalid '{type:"url",value:$f.valid.content_ref} | schema::artifact_ref_ok' +expect_false artifact-ref-nested-newline '{type:"content",value:($f.valid.content_ref + {content_id:"content.example\n"})} | schema::artifact_ref_ok' mark_rule portable-core-schema.artifact-ref expect_true input-ref-variants '({type:"artifact",value:{type:"content",value:$f.valid.content_ref}} | schema::input_ref_ok) and ({type:"document",value:$f.valid.document_ref} | schema::input_ref_ok)' expect_false input-ref-invalid '{type:"document",value:$f.valid.content_ref} | schema::input_ref_ok' +expect_false input-ref-nested-newline '{type:"document",value:($f.valid.document_ref + {id:"result.example\n"})} | schema::input_ref_ok' mark_rule portable-core-schema.input-ref expect_true evidence-ref-valid '{stage_result_ref:$f.valid.document_ref,evidence_id:"evidence.example"} | schema::evidence_ref_ok' expect_false evidence-ref-kind '{stage_result_ref:($f.valid.document_ref + {kind:"profile"}),evidence_id:"evidence.example"} | schema::evidence_ref_ok' +expect_false evidence-ref-id-newline '{stage_result_ref:$f.valid.document_ref,evidence_id:"evidence.example\n"} | schema::evidence_ref_ok' mark_rule portable-core-schema.evidence-ref expect_true scope-ref-valid '$f.valid.scope_ref | schema::scope_ref_purpose_ok("policy")' expect_false scope-ref-invalid '$f.valid.scope_ref + {purpose:"trust"} | schema::scope_ref_ok' +expect_false scope-ref-purpose-newline '$f.valid.scope_ref + {purpose:"policy\n"} | schema::scope_ref_ok' +expect_false scope-ref-sha-newline '$f.valid.scope_ref + {scope_sha256:($f.valid.scope_ref.scope_sha256 + "\n")} | schema::scope_ref_ok' mark_rule portable-core-schema.scope-ref expect_true actor-ref-valid '{role:"producer",implementation_id:"implementation.example",implementation_version:"v1",adapter_instance_id:"instance.example",principal_id:"principal.example",execution_boundary_id:"boundary.example",authority_ref:$f.valid.authority_ref} | schema::actor_ref_ok' expect_false actor-ref-invalid '{role:"human",implementation_id:"implementation.example",implementation_version:"v1",adapter_instance_id:"instance.example",principal_id:"principal.example",execution_boundary_id:"boundary.example"} | schema::actor_ref_ok' +expect_false actor-ref-id-newline '{role:"producer",implementation_id:"implementation.example\n",implementation_version:"v1",adapter_instance_id:"instance.example",principal_id:"principal.example",execution_boundary_id:"boundary.example"} | schema::actor_ref_ok' mark_rule portable-core-schema.actor-ref expect_true environment-ref-valid '{environment_id:"environment.example",fingerprint_sha256:$f.valid.sha256} | schema::environment_ref_ok' expect_false environment-ref-invalid '{environment_id:"environment.example",fingerprint_sha256:"bad"} | schema::environment_ref_ok' +expect_false environment-ref-newline '{environment_id:"environment.example\n",fingerprint_sha256:$f.valid.sha256} | schema::environment_ref_ok' mark_rule portable-core-schema.environment-ref expect_true tool-ref-valid '$f.valid.tool_ref | schema::tool_ref_ok' expect_false tool-ref-invalid '$f.valid.tool_ref + {config_ref:{state:"present",value:$f.valid.content_ref}} | schema::tool_ref_ok' +expect_false tool-ref-version-newline '$f.valid.tool_ref + {tool_version:"v1\n"} | schema::tool_ref_ok' mark_rule portable-core-schema.tool-ref expect_true git-patch-valid '$f.valid.patch_ref | schema::git_patch_ref_ok' @@ -384,19 +413,23 @@ mark_rule portable-core-schema.git-patch-ref expect_true change-ref-valid '{repository_id:"repo.example",base:{state:"absent"},head:$f.valid.git_revision_sha1,delta_ref:$f.valid.patch_ref} | schema::change_ref_ok' expect_false change-ref-repository '{repository_id:"other.example",base:{state:"absent"},head:$f.valid.git_revision_sha1,delta_ref:$f.valid.patch_ref} | schema::change_ref_ok' +expect_false change-ref-repository-newline '{repository_id:"repo.example\n",base:{state:"absent"},head:$f.valid.git_revision_sha1,delta_ref:$f.valid.patch_ref} | schema::change_ref_ok' mark_rule portable-core-schema.change-ref expect_true source-value-valid '{source:$f.valid.git_blob,value_format:"canonical-json",value_sha256:$f.valid.sha256} | schema::source_value_ref_ok' expect_false source-value-tree '{source:$f.valid.git_tree,value_format:"canonical-json",value_sha256:$f.valid.sha256} | schema::source_value_ref_ok' +expect_false source-value-sha-newline '{source:$f.valid.git_blob,value_format:"canonical-json",value_sha256:($f.valid.sha256 + "\n")} | schema::source_value_ref_ok' mark_rule portable-core-schema.source-value-ref mark_test portable-core-schema.test.legacy-127-canonical-json-source-pointing-at-a-tree-not-a-blob expect_true delivered-scope-valid '{ref:{purpose:"output-contract",decision_record_ref:$f.valid.content_ref,subject_ref:{type:"artifact",value:{type:"content",value:$f.valid.content_ref}},scope_sha256:$f.valid.sha256},input_id:"input.example"} | schema::delivered_scope_ok("output-contract")' expect_false delivered-scope-document '{ref:{purpose:"output-contract",decision_record_ref:$f.valid.content_ref,subject_ref:{type:"document",value:$f.valid.document_ref},scope_sha256:$f.valid.sha256},input_id:"input.example"} | schema::delivered_scope_ok("output-contract")' +expect_false delivered-scope-input-newline '{ref:{purpose:"output-contract",decision_record_ref:$f.valid.content_ref,subject_ref:{type:"artifact",value:{type:"content",value:$f.valid.content_ref}},scope_sha256:$f.valid.sha256},input_id:"input.example\n"} | schema::delivered_scope_ok("output-contract")' mark_rule portable-core-schema.delivered-scope expect_true fact-variants '({state:"recorded",value:"value.example",source_ref:$f.valid.content_ref} | schema::fact_ok(schema::id_ok)) and ({state:"computed",value:"value.example",source_ref:$f.valid.content_ref} | schema::fact_ok(schema::id_ok)) and ({state:"unavailable",reason_id:"reason.example"} | schema::fact_ok(schema::id_ok)) and ({state:"not-applicable"} | schema::fact_ok(schema::id_ok))' expect_false fact-invalid '{state:"recorded",value:"value.example"} | schema::fact_ok(schema::id_ok)' +expect_false fact-reason-newline '{state:"unavailable",reason_id:"reason.example\n"} | schema::fact_ok(schema::id_ok)' mark_rule portable-core-schema.fact-union schema_route_total=13 @@ -420,7 +453,7 @@ else fi mark_rule portable-core-schema.schema-route-all-docs -schema_registry_total=7 +schema_registry_total=8 schema_canonical_registry="$schema_test_tmp/registry.canonical.json" "$schema_jq" -S -c . "$schema_registry" > "$schema_canonical_registry" if cmp -s "$schema_registry" "$schema_canonical_registry"; then @@ -439,15 +472,29 @@ else fi if "$schema_jq" -e ' all(.[]; (keys | sort) == ["generation_id","parent_plan_merge_commit","parent_spec_blob"] and - (.generation_id | test("^g-[0-9a-f]{64}$")) and - (.parent_spec_blob | test("^[0-9a-f]{40}$|^[0-9a-f]{64}$")) and - (.parent_plan_merge_commit | test("^[0-9a-f]{40}$|^[0-9a-f]{64}$"))) and + (.generation_id | test("\\Ag-[0-9a-f]{64}\\z")) and + (.parent_spec_blob | test("\\A([0-9a-f]{40}|[0-9a-f]{64})\\z")) and + (.parent_plan_merge_commit | test("\\A([0-9a-f]{40}|[0-9a-f]{64})\\z"))) and (map(.generation_id) | length) == (map(.generation_id) | unique | length) ' "$schema_registry" >/dev/null; then schema_registry_passed=$((schema_registry_passed + 1)) else fail_case "registry entry shape and uniqueness" fi +if "$schema_jq" -e ' + def entry_ok: + (.generation_id | test("\\Ag-[0-9a-f]{64}\\z")) and + (.parent_spec_blob | test("\\A([0-9a-f]{40}|[0-9a-f]{64})\\z")) and + (.parent_plan_merge_commit | test("\\A([0-9a-f]{40}|[0-9a-f]{64})\\z")); + (.[0] | entry_ok) and + ((.[0] | .generation_id += "\n") | entry_ok | not) and + ((.[0] | .parent_spec_blob += "\n") | entry_ok | not) and + ((.[0] | .parent_plan_merge_commit += "\n") | entry_ok | not) + ' "$schema_registry" >/dev/null; then + schema_registry_passed=$((schema_registry_passed + 1)) +else + fail_case "registry absolute-anchor trailing-newline rejection" +fi if "$schema_jq" -e --arg base "$schema_base" \ '.metadata.construction_base == $base and .metadata.prior_registry_entries == 0' \ "$schema_fixture" >/dev/null; then @@ -514,7 +561,7 @@ guard_paths_ok() { done < "$paths_file" } -schema_guard_total=25 +schema_guard_total=35 schema_generation_files="$schema_test_tmp/generation-files" find "$schema_root/core/v1/generations/$schema_generation" -type f -print | \ sed "s#^$schema_root/##" | LC_ALL=C sort > "$schema_generation_files" @@ -535,7 +582,9 @@ if [ -d "$schema_module_dir" ] && [ ! -L "$schema_module_dir" ] && else fail_case "private generation path must be real directories and regular files" fi -if ! grep -Eq '^[[:space:]]*(import|include|module)[[:space:](]' "$schema_module"; then +schema_module_directive_pattern='(?:^|[;\r\n])[[:space:]]*(?:import|include)[[:space:]]*("(?:[^"\\]|\\.)*")' +if ! "$schema_jq" -Rse --arg pattern "$schema_module_directive_pattern" \ + 'test($pattern)' < "$schema_module" >/dev/null; then schema_guard_passed=$((schema_guard_passed + 1)) else fail_case "schema module must remain import-free" @@ -579,14 +628,16 @@ schema_live_hits="$schema_test_tmp/live-hits" schema_import_hits="$schema_test_tmp/import-hits" : > "$schema_live_hits" : > "$schema_import_hits" -schema_import_pattern='(^|[^A-Za-z0-9_])(import|include)[[:space:]]*"schema"' while IFS= read -r -d '' schema_tracked_path; do if ! schema_scan_result="$( git -C "$schema_root" show ":$schema_tracked_path" 2>/dev/null | "$schema_jq" -Rsr \ --arg generation "$schema_generation" \ - --arg import_pattern "$schema_import_pattern" \ - '[contains($generation),test($import_pattern)] | @tsv' + --arg module_pattern "$schema_module_directive_pattern" \ + '[contains($generation), + ([scan($module_pattern)] | + any(.[]; ((.[0] | try fromjson catch "") == "schema")))] | + @tsv' )"; then fail_case "unable to scan tracked path: $schema_tracked_path" continue @@ -631,8 +682,10 @@ schema_load_case() { local source_file="$schema_test_tmp/$case_id.jq" local actual=false printf '%s' "$source_text" > "$source_file" - if "$schema_jq" -Rse --arg pattern "$schema_import_pattern" \ - 'test($pattern)' < "$source_file" >/dev/null; then + if "$schema_jq" -Rse --arg pattern "$schema_module_directive_pattern" \ + '[scan($pattern)] | + any(.[]; ((.[0] | try fromjson catch "") == "schema"))' \ + < "$source_file" >/dev/null; then actual=true fi if [ "$actual" = "$expected" ]; then @@ -647,12 +700,50 @@ schema_load_case import-compact true $'import"schema"as s;\n' schema_load_case import-arbitrary-alias true $'import "schema" as any_alias_42;\n' schema_load_case import-newlines true $'import\n "schema"\n as\n another_alias;\n' schema_load_case import-metadata true $'import"schema"as s {search:"."};\n' +schema_load_case import-escaped-module true $'import"sch\\u0065ma"as escaped;\n' schema_load_case include-spaced true $'include "schema";\n' schema_load_case include-compact true $'include"schema";\n' schema_load_case include-newlines true $'include\n "schema" ;\n' schema_load_case other-module false $'import "profile_graph" as schema;\n' schema_load_case longer-module false $'include "schema-extra";\n' schema_load_case keyword-substring false $'myimport"schema"as s;\n' + +schema_directive_case() { + local case_id="$1" + local expected="$2" + local source_text="$3" + local source_file="$schema_test_tmp/$case_id.jq" + local actual=false + printf '%s' "$source_text" > "$source_file" + if "$schema_jq" -Rse --arg pattern "$schema_module_directive_pattern" \ + 'test($pattern)' < "$source_file" >/dev/null; then + actual=true + fi + if [ "$actual" = "$expected" ]; then + schema_guard_passed=$((schema_guard_passed + 1)) + else + fail_case "$case_id expected module-directive=$expected, got module-directive=$actual" + fi +} + +schema_directive_case foreign-import-compact true \ + $'import"profile_graph"as profile_graph;\n' +schema_directive_case foreign-import-spaced true \ + $'import "stage_request" as stage_request;\n' +schema_directive_case foreign-import-multiline true \ + $'import\n "result_facts"\n as result_facts;\n' +schema_directive_case foreign-import-metadata true \ + $'import"result_truth"as result_truth {search:"."};\n' +schema_directive_case foreign-include-compact true \ + $'include"profile_graph";\n' +schema_directive_case foreign-include-multiline true \ + $'include\n "stage_request";\n' +schema_directive_case prose-import false \ + $'This prose says import "profile_graph" as profile_graph.\n' +schema_directive_case string-import false \ + $'"import\\"profile_graph\\"as profile_graph;"\n' +schema_directive_case comment-include false \ + $'# include "profile_graph";\n' if guard_paths_ok "$schema_generation_files"; then schema_guard_passed=$((schema_guard_passed + 1)) else