From c7e8d9e595c1f95687cba41ace1cc8eeb45a2a47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Z=20Sink=C3=B3?= Date: Mon, 2 Nov 2026 08:12:40 +0100 Subject: [PATCH] feat(release): project.yaml declares the 30 approved release artifacts compiler_settings.release_artifacts.paths is the list the owner approved as the published surface (docs/en/RELEASE-ARTIFACT-CLASSIFICATION.md, 30 INCLUDE of 131 files). tests/test_release_declaration.py is the evidence: the declaration equals the approved list, every path is a regular file in the commit, every schema the compiler would publish is listed, the twelve Forms-pinned files are a subset, and the compiler produces the same artifacts block on every run and in every order, equal to one computed from the git blobs. It does not show that a release from this commit contains the pinned bytes (P0-4-MIGRATION.md differs), and it authorizes no release. --- [signing-metadata] key = cic-my-sign-key signature = vault:v1:MEUCIElSQsjpG0/F7h5VwYckeriZeuuix5sGUcpF0PorZwKMAiEAwYizhGQ6tUzs9UsQEMNhy4ACrm7PZR2kfVvM0tXFHgg= hash-algorithm = sha256 digest = OyueUXNPghqbyi5xWgrvg1MYs6wbZAHq0BbOR4CdwUA= [certificate] -----BEGIN CERTIFICATE----- MIICBjCCAaygAwIBAgIUSnRMR6RPnEbg296XWPOqq/u5PCwwCgYIKoZIzj0EAwIw QzELMAkGA1UEBhMCSFUxGTAXBgNVBAoMEENlbnRyYWxJbmZyYUNvcmUxGTAXBgNV BAMMEENJQyBEZXZlbG9wZXIgQ0EwHhcNMjYwMzIwMTMyMjU5WhcNMjYxMjMxMTMy MjU5WjBFMQswCQYDVQQGEwJIVTEZMBcGA1UECgwQQ2VudHJhbEluZnJhQ29yZTEb MBkGA1UEAwwSR2Fib3IgWm9sdGFuIFNpbmtvMFkwEwYHKoZIzj0CAQYIKoZIzj0D AQcDQgAEIG2CVmTfmLB9pLLclj7YmP2eedAjklpy4LGrU2ijoiy6Xqpuybv7OgJe i+ez31s65NEV8+X/ByeX1cstR988z6N8MHowCQYDVR0TBAIwADAdBgNVHQ4EFgQU yZN6AIX/TNnIJ9GwAa/NRN3ujHAwHwYDVR0jBBgwFoAUXn6CHYzPUqU4JVP8g+OS WeDYjhcwDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEF BQcDBDAKBggqhkjOPQQDAgNIADBFAiEA+bFzXRoJ4PCQbhAAtpkcMjt0vNj5rEW0 lOMBGDNyaWkCIB1vmM7PcZzv/c9bIrxF5kqv6QXomouhByUfeNUTbpKW -----END CERTIFICATE----- --- project.yaml | 34 ++++++++ tests/test_release_declaration.py | 140 ++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 tests/test_release_declaration.py diff --git a/project.yaml b/project.yaml index 81a1b85..c1192fd 100644 --- a/project.yaml +++ b/project.yaml @@ -22,5 +22,39 @@ compiler_settings: vault_cert_path: "cic-my-sign-key/crt/bar" owner_email: sinkog@centralinfracore.hu validity_days: 365 + # The files a release publishes (docs/en/RELEASE-ARTIFACT-CLASSIFICATION.md: the owner's decision of 2026-11-01, 30 paths). Explicit paths, relative to the + # repository root, no globs; the compiler reads this declaration from the SOURCE COMMIT's project.yaml and resolves the paths against that commit's git tree. + release_artifacts: + paths: + - LICENSE.md + - dependency.yaml + - docs/en/RELEASE-RECORD-CONTRACT.md + - md.meta.schema.yaml + - proposals/atom-grammar/P0-4-ACCESS-GRAMMAR.md + - proposals/atom-grammar/P0-4-MIGRATION.md + - proposals/atom-grammar/README.md + - proposals/atom-grammar/access-vectors/expansion.yaml + - proposals/atom-grammar/access-vectors/migration.yaml + - proposals/atom-grammar/access-vectors/reading.yaml + - proposals/atom-grammar/access-vectors/resolution.yaml + - proposals/atom-grammar/access_policy.py + - proposals/atom-grammar/check_access_vectors.py + - proposals/atom-grammar/check_grammar.py + - proposals/atom-grammar/instance-grammar.schema.yaml + - release.schema.yaml + - schemas/aggregate/config-surface.yaml + - schemas/aggregate/managed-entity.yaml + - schemas/aggregate/operation-surface.yaml + - schemas/aggregate/policy-surface.yaml + - schemas/aggregate/state-surface.yaml + - schemas/atomic/access.yaml + - schemas/atomic/address.yaml + - schemas/atomic/behavior.yaml + - schemas/atomic/contract.yaml + - schemas/atomic/event.yaml + - schemas/atomic/identity.yaml + - schemas/atomic/role.yaml + - schemas/atomic/shape.yaml + - schemas/index.yaml x-cic: repo_type: primitive diff --git a/tests/test_release_declaration.py b/tests/test_release_declaration.py new file mode 100644 index 0000000..65ef2c8 --- /dev/null +++ b/tests/test_release_declaration.py @@ -0,0 +1,140 @@ +"""The declaration of the files this repository publishes (project.yaml, compiler_settings.release_artifacts.paths), measured. + +The list was approved by the owner as content (docs/en/RELEASE-ARTIFACT-CLASSIFICATION.md, 30 paths). This file is the evidence that the declaration in project.yaml IS that list +and that it works with the compiler of this repository: the paths exist in the commit under test, every schema the compiler would publish as a `spec` is among them, the twelve +files of the Forms grammar pin are a subset, and the artifacts block the compiler produces from it is the same on every run and equals one computed here from the git blobs. + +What it does NOT show, deliberately: that a release cut from this commit contains the bytes the Forms pin names (the pin is a historical commit; eleven of the twelve files are +identical at this commit, P0-4-MIGRATION.md is not, and nothing here pretends otherwise), that a release may be made, or that anything is authentic. The release branch from the +historical pin is another commit state with its own declaration and its own evidence. +""" +import hashlib +import itertools +import pathlib +import re +import subprocess +import sys + +import pytest +import yaml +from jsonschema import validate + +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent)) +import e3_fixtures as fx # noqa: E402 + +from tools import compiler # noqa: E402 + +ROOT = pathlib.Path(__file__).resolve().parents[1] +V = fx.load_verifier() + + +def git(*args): + return subprocess.run(["git", "-C", str(ROOT)] + list(args), capture_output=True, check=True).stdout + + +@pytest.fixture(scope="module") +def commit(): + return git("rev-parse", "HEAD").decode().strip() + + +@pytest.fixture(scope="module") +def tree(commit): + return compiler._git_tree(commit) + + +@pytest.fixture(scope="module") +def declared(): + project = yaml.safe_load((ROOT / "project.yaml").read_text(encoding="utf-8")) + return project["compiler_settings"]["release_artifacts"]["paths"] + + +def approved_paths(): + """The approved content: the yaml block of the classification document, which the owner's decision produced.""" + doc = (ROOT / "docs" / "en" / "RELEASE-ARTIFACT-CLASSIFICATION.md").read_text(encoding="utf-8") + block = re.search(r"```yaml\n(.*?)```", doc, re.S).group(1) + return yaml.safe_load(block)["compiler_settings"]["release_artifacts"]["paths"] + + +def forms_pin_paths(): + pin = yaml.safe_load((ROOT / "tests" / "fixtures" / "forms-pin-73ae469.yaml").read_text(encoding="utf-8")) + return [f["path"] for f in pin["grammar"]["cic-primitives-73ae469-6a188078"]["files"]] + + +def test_the_declaration_is_the_list_the_owner_approved(declared): + assert len(declared) == 30 + assert declared == approved_paths() + + +def test_the_declaration_is_explicit_unique_and_sorted_by_utf8_bytes(declared): + assert len(set(declared)) == len(declared) + assert declared == sorted(declared, key=lambda p: p.encode("utf-8")) + assert not any(ch in p for p in declared for ch in "*?[]{}") + + +def test_the_compiler_accepts_the_declaration_as_the_commit_holds_it(commit, tree): + project = compiler._declaration_from_commit(tree) + assert compiler._validate_release_artifact_declaration(project, tree) is None + assert V.validate_declaration(project, tree) is None # the reference reading agrees + + +def test_every_path_is_a_regular_file_in_the_commit_under_test(declared, tree): + for path in declared: + assert path in tree, path + mode, kind, _oid = tree[path] + assert kind == "blob" and mode in ("100644", "100755"), (path, mode, kind) + + +def test_every_schema_the_compiler_would_publish_as_a_spec_is_listed(declared, tree): + specs = compiler._release_schema_files_in_tree(tree) + assert len(specs) == 13 + assert set(specs) <= set(declared), sorted(set(specs) - set(declared)) + + +def test_the_working_directorys_spec_selection_is_the_same_and_listed(declared): + # in a clean checkout (the gate's) the selection the compiler makes from the working directory is the one it makes from the commit + assert set(compiler._release_schema_files()) <= set(declared) + + +def test_the_twelve_files_of_the_forms_grammar_pin_are_a_subset(declared): + pin = forms_pin_paths() + assert len(pin) == 12 + assert set(pin) <= set(declared), sorted(set(pin) - set(declared)) + # the other way round is not required: the producer's list is its own, not the consumer's + assert len(set(declared) - set(pin)) == 18 + + +def test_the_artifacts_block_is_the_same_on_every_run_and_in_every_declared_order(declared, tree): + blocks = {repr(compiler._build_release_artifacts(list(order), tree)) + for order in (declared, list(reversed(declared)), sorted(declared), sorted(declared, key=lambda p: p[::-1]))} + assert len(blocks) == 1 + # and a handful of random-looking permutations of the first six, to make the order argument + first = declared[:6] + assert len({repr(compiler._build_release_artifacts(list(p) + declared[6:], tree)) for p in itertools.islice(itertools.permutations(first), 40)}) == 1 + + +def test_the_block_equals_one_computed_here_from_the_git_blobs(declared, commit, tree): + block = compiler._build_release_artifacts(declared, tree) + files = [{"path": p, "digest": "sha256:" + hashlib.sha256(git("show", "%s:%s" % (commit, p))).hexdigest()} for p in sorted(declared, key=lambda p: p.encode("utf-8"))] + assert block["files"] == files + assert block["manifest_digest"] == fx.independent_manifest_digest(files) + + +def test_the_reference_verifier_reads_that_block_as_a_conforming_artifact_list(declared, commit, tree): + block = compiler._build_release_artifacts(declared, tree) + record = {"provenance": {"envelope": 3, "source_commit": commit}, "release": {"envelope": 3}, "artifacts": block} + for check in (V._check_source_commit, V._check_artifacts_present, V._check_path_form, V._check_path_duplicate, V._check_order, V._check_digest_form, + V._check_manifest_digest_form, V._check_manifest): + assert check(record) is None, check.__name__ + blobs = V.check_blobs(record, str(ROOT)) + assert blobs.status == "PASS", blobs.failures + + +def test_the_real_project_yaml_meets_its_schema(): + project = yaml.safe_load((ROOT / "project.yaml").read_text(encoding="utf-8")) + validate(instance=project, schema=yaml.safe_load((ROOT / "project.schema.yaml").read_text(encoding="utf-8"))) + + +def test_the_declaration_names_no_consumer_and_no_generated_or_tooling_file(declared): + for path in declared: + assert not path.startswith(("tests/", "tools/", ".github/", "ai/", "features/", "release/")), path + assert path not in ("project.yaml", "commitment.yaml", "requirements.in", "requirements.txt"), path