From 310760db73b53f8db0e6b37cc89e1f58a57548b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Z=20Sink=C3=B3?= Date: Mon, 10 Aug 2026 16:51:01 +0200 Subject: [PATCH] docs: the four decisions and the review ledger, which #24 merged without MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #24 merged an earlier head than the branch carried. Measured against devel after the merge: zero DECIDED blocks, no trust-boundary section, no disposition ledger, no reconciliation tool. Eleven files, 462 lines, none of it there. This is the second time a merge has taken a head that was not the branch's. The first was #8, which merged into a base that had already merged to main and left a commit behind. Both were invisible until something was looked for and was not found — a force-push and a merge race, and no check that compares what a PR claims to carry with what the target ends up holding. Nothing here re-derives anything: the files are the same content, restored onto current devel and the derived values recomputed. WHAT WAS LOST AND IS BACK: Four decisions, each with what was refused and why. D-1 `scalar_type` CONSTRAINS, no coercion, and belongs to D-5. A contract cannot be evaluated without a type — `range: [1, 100]` against "50" has no answer — so a descriptive scalar_type leaves the evaluator inventing its own typing and the model carrying two type systems. D-2 REJECTION. A complete extracted set crosses into a module, so a declared position with no value is not a state the boundary can be in. `required` keeps a sharper meaning: the INSTANCE must supply it, expressible as a member that may not carry `origin: [schema]`. D-5 ENFORCE `access` AND `contract`. One question, answered once. D-6 RESTRICT declared names; opaque keys exempt and not addressable, because nothing references into an opaque blob. And the section three of them turn on, which was written down nowhere: `Materialize` IS the pre-entry step, so the untrusted edge is there and at `ValidateCanonicalDocument` — not at the module boundary, which receives a complete set. Resource budgets belong at the first two. The review ledger and its gate: reviews/cbaf928.disposition.md carries one row per finding, and tools/check_review_ledger.py reconciles the findings the threads raise, the ledger rows, and the decisions. It exists because the decisions list lost two findings — claim/F-08 and claim/F-10 were raised, measured, confirmed, and then appeared nowhere. 14 closed, 10 partial, 13 open, of 37. A partial is not a closed one. Plus the three false claims: `make conformance` calling a target that does not exist, "32 of 34 invariants" against 46 and 40, and "no module and no boundary" long after go/module/ existed. make ci passes. --- [signing-metadata] key = cic-my-sign-key signature = vault:v1:MEYCIQDfGXEN6FnkqEvs2RD3wz4TioMEi4U+bglaEUD8ROUZ+gIhAKhL3VzLDE278w8BUr2o/iQ2oZADxXo9/F55BTGlnewa hash-algorithm = sha256 digest = DtL0VA5vNUIrMEx3Ftr9eGlPm6j6v5GUBj/9RUUxKOA= [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----- --- MANIFEST.sha256 | 18 ++-- Makefile | 8 +- SPEC.md | 2 +- docs/pending-decisions.md | 168 +++++++++++++++++++++++++++++++++ docs/spec-vector-map.md | 11 ++- mk/ci.mk | 2 +- project.yaml | 2 +- reviews/README.md | 16 ++++ reviews/cbaf928.disposition.md | 64 +++++++++++++ tools/check_review_ledger.py | 160 +++++++++++++++++++++++++++++++ tools/check_status_claims.py | 29 +++++- 11 files changed, 462 insertions(+), 18 deletions(-) create mode 100644 reviews/cbaf928.disposition.md create mode 100644 tools/check_review_ledger.py diff --git a/MANIFEST.sha256 b/MANIFEST.sha256 index 9ebbc88..8315cc3 100644 --- a/MANIFEST.sha256 +++ b/MANIFEST.sha256 @@ -36,7 +36,6 @@ 168cd37e606cb1646305d512096290e6aeeb612997a8d0200280f821025dc79d go/objectmodel/errors.go 16a0a5ade85c937b51c4a7e5131eb8b0b9b03e4bfb19dd3d0654c9c7b6f44aa0 conformance/materialization/007_normalize_scalar/meta.yaml 171735e8e595777d10212f048329f6d0ca0885b79df841d55ce34adc2466a26c conformance/materialization/010_normalize_empty_object/input.yaml -1774b9631c1cf1b7abdb9a651277bb03dc71454524376a482f63d86def993bb1 Makefile 1b3e25bf2122e2d44a8cfc13620797fd129911c5c74d8e1c1db96cee6994f1ce docs/hu/concept/git-management.meta.yaml 1cbf2e8be1e4909a51180757ab9362abe0b8212025a4ea63d60d1f9a4c15ca6a tests/test_tools/test_compiler.yaml 1d446b9b82e3a4e641645bb34a371dd42063f8732044d88108dd1715e9238a83 conformance/validation/008_origin_terms_out_of_order/meta.yaml @@ -45,7 +44,6 @@ 1fb7dd2afe2b230d7c335db5c5da4e72e0ad271ce2dcd1683f446945e0abb917 conformance/materialization/011_discriminator_envelope/input.yaml 2020cd3da7008987b81055f0ef1bc7481567202da7f8bf49d616b1018f52bc21 mk/infra.mk 2063bf8ac333858a9b5c96de552fc4879ca3c35e5d0e34b0dd4067cec94bb5da Dockerfile -214885986c75f2a6928c9e2a8326804353310ade3834815c5d64f3e74634ddbf project.yaml 21a2f539315cb8500c9d812776d0e777b2eca73d668b3079b694b651fe8539d8 conformance/materialization/012_discriminator_payload_keywords/input.yaml 21a9994fa283ffca9aa0d063526ba4bc8b7d53d781bfbbee0d0041dcc04d143a conformance/invalid/005_origin_in_authoring_input/schema.yaml 21a9994fa283ffca9aa0d063526ba4bc8b7d53d781bfbbee0d0041dcc04d143a conformance/invalid/006_unknown_primitive/schema.yaml @@ -53,7 +51,6 @@ 225e266950c6f85bfd2be20cacb3de0ac011414f92010b831136640ffe9a46be conformance/invalid/007_sealed_missing_path/expected-error.yaml 229376bac93d6b5d81c8c3015b4981999cf3a3c82bf8a8e1d63eecaf61d1e35d conformance/validation/007_origin_duplicate_term/expected-error.yaml 24219339b219898ecb69d09495bc078745cef2c1b5065eedfff886dd95032881 conformance/invalid/010_required_value_missing/expected-error.yaml -2758d20cf5aa0688060bce28a52786eee272986714a8376d7f190eb2dc90a43e reviews/README.md 275acc4de6f4d5fcd87330039777ceb8f8d8a7933c4e4924eccabe21a9f28da2 conformance/materialization/002_origin_schema/meta.yaml 27fc384a0d7987e4cb311648408b9b5a3995c0b58871ed55389ae154061209c9 conformance/validation/011_more_than_one_document/object.yaml 289ba61b1eec45ea29706ab25d49c8321c537ee7b2a509485bce0d206ef5d808 conformance/validation/004_documentation_member/meta.yaml @@ -83,6 +80,7 @@ 3aa343e865f65a1c38d7caf7e87237e3cb31eff799ecbeb11bf7de1539c0d766 docs/branch-decision.md 3ae763882179fe25648851b319adc020621c33e3d58558a9922cb34be1987bd1 .github/workflows/ci.yml 3b8aa382e06a8c03e74304dca7f8e7de9891c1504a1134b2774e5f84593d0c2e tools/compiler.yaml +3c810d01d23750a1cd815131cc3f3891257f67155b12b0c69949057a6c3fb3f4 reviews/README.md 3d22e1f391a04e7cbef7dc401c3901a482ab361bdc0879c8b69ab231fe4a1371 conformance/materialization/008_normalize_list/schema.yaml 401e20e529bf4645e1ea75fb59bda0082e8607cee306b0b3e8ceb503945a10f4 conformance/materialization/011_discriminator_envelope/schema.yaml 404cc649efa184f8caa6cc9280cf078081eea60a67257d7bc2996cce3168aaf6 conformance/invalid/011_list_position_scalar_payload/meta.yaml @@ -99,12 +97,14 @@ 475c277c385ff0cb55248bbfaf1888a4dcd86c0d9f2e8661eaffe6616c0a0e60 rust/src/canonical.rs 477943bac345e5da068ae948891595f6c3659961a144dc0f8495836a9bdc84f8 conformance/materialization/005_closure_structured/input.yaml 4798b99c0448d4e338bdb21567d8fee0aef1d0751f49315f3f3fb6ea986f2c38 conformance/materialization/014_hostile_keys/meta.yaml +47c67a390720dfc0c4879f0a2652302e3b772a0bce73324bd3fbd602b86608df SPEC.md 490aad144271534a93573d970887b60d36e2aa5648f955e522225a5d2ca729c9 conformance/invalid/012_scalar_position_sequence_payload/schema.yaml 490aad144271534a93573d970887b60d36e2aa5648f955e522225a5d2ca729c9 conformance/materialization/007_normalize_scalar/schema.yaml 4d22a4da883cc4a5aa0e0401e5af26d7ae67779a464610343920d8bfcd799ff9 rust/tests/cli.rs 4fe73a4abf70523602e1fded96685b014d67e098f63c9fbdde1b3c26f97a8dcf conformance/invalid/008_duplicate_mapping_key/input.yaml 4fffcdac095c0623fefb8515ab80b3e9629c0febc454456cd08b3cdf3c941720 LICENSE.md 5028d0500342d09e11c1771e2c3500d0710576d82cb08d42273d8d64568d1b91 conformance/materialization/005_closure_structured/expected.yaml +5171e5e847c0d9a74be405382923c62c6e3bb92947f3f8476f65ec8b4b73e074 project.yaml 51b2d42be6a1e6e2f7449f3c0bf29f5a151910ef79db7d67bfcbe257a3642831 docs/decision-delta.md 528112e3921c47f13c7ec8ef080dd6f59d0b6d1610d17047fba7fb46c69f2e05 tools/mutate.py 534ca57fb760f1e30ec4d790a4e9ab71a228b4d0dbe8ac48c4c0ddf5dbcb37b9 conformance/materialization/006_closure_opaque/meta.yaml @@ -155,7 +155,6 @@ 72c48fa014dea46236640cf52037631aa5db63a6f58807b3bf24577a247aa238 tools/check_doc_links.yaml 72e9b7c69632c1e1877e9f9d69812f3259003989b69f6b0fa7f23612d4514c94 tools/releaselib/git_service.yaml 7426752feb1140b2023192728df35248e4f802617c150bb0523ab97dc1638c0b tools/init-hooks.sh -753f2eb2a6b70c2e35e76b61baa67b9f9533f8f86713da7b9ff62ab44def6ccb docs/pending-decisions.md 75b59a6cf6a2ce8af4fd04c8a0dcf7d33e5e5f132a48a981252ce61c05cf97c9 docs/hu/makefile-cheatsheet.md 75d086d878abe5943b2dd0c080c0ffb4fd88b517289c216ffa6bc5c1f12056f7 conformance/invalid/003_closure_undeclared/input.yaml 764d101d6bb896af8c4f082768f30f9f7cf77a6f0a0fed940b7b39805330d842 requirements.txt @@ -179,7 +178,9 @@ 8364e5bdf9d568a1c94ea1414673bd584c0fceb125f15690269d3505a5f2e17a conformance/materialization/004_origin_sealed_schema/expected.yaml 83af0f2f2e46f877594a330342b4336cf48b996715939e422c40824e6507aad8 conformance/invalid/001_sealed_yaml_conflict/schema.yaml 83eb6b0f9847e5ac3102a79e49afecb3fe9459f6da0fd2db12e7692bc448615d conformance/validation/001_origin_yaml_schema_conflict/object.yaml +83f7e5e490bb7a136566df690b940622025c4bf75ef5f76f399c633328545ba9 docs/pending-decisions.md 8511de46e89723d35ea8a6ba6ebd75a86e73a61d4ef5020784217056aa346fee conformance/invalid/001_sealed_yaml_conflict/expected-error.yaml +857a5ff6fc1905a334ec14d09f8355b4cc8f0d472b381387dcb78603b9620fcc mk/ci.mk 85a21ef03ffdf0f4edd2612ce71c044c95361b808a0d3f93a9ea26ba2f23a8e0 spec/index.yaml 85cc259951a5be28438a1a7fa84d2ec7964e02f36f7ffd846ee886e77c69097d conformance/invalid/004_schema_declares_values_child/schema.yaml 85db5ead37f57de271fde52d9c61048fcaacd80d1d66221f0c121cc2ccb96968 go/objectmodel/entry.yaml @@ -203,6 +204,7 @@ 8de5e87b52c306eae98af3bcf5b940d0e87f90087878cb51efb54518a896503b tools/schemalib/__init__.yaml 8e53ccf0da86dea86513645275f18542579434cce02c8227ad8a8de4c82874ad tests/test_tools/test_releaselib/test_vault_service.yaml 90500311a3cb76038f37c3de19ad25b4b0404ddef84fa7d5706966cf15cc49f2 docs/migration-surface.md +9148b50133d976376292f4c1b05b569c7d6fe8c20116615c07c933228dd2f361 reviews/cbaf928.disposition.md 91d327f1771e52ad4ffb858b5889338c95bbb340f3419cd996708822c2b1e5d8 conformance/invalid/008_duplicate_mapping_key/expected-error.yaml 91e1b1394a19b9ab15ce1b4600c551341a8ccecd22d9b9cc8e4cbb0124ff6949 conformance/invalid/004_schema_declares_values_child/meta.yaml 92beb5273b4f3b912edfe35ee860eb4e01e6524504ceb5c86ab4dda9e330a291 tools/__init__.yaml @@ -212,14 +214,12 @@ 94dabea4d0bdb7a81c750c4b2830f096c9ee2e59952fd1b9f7d4e235cddeb2f9 mk/golang.mk 96a0612d7f3d7d3faf96bbc408da0b32979eaee2599962e8d6d9e97c15b38d40 conformance/materialization/015_nested_sealed_template/schema.yaml 992423396d17250586acefe1ca589c21d650fb75c7f578bdff9cbb42fa1351fa conformance/invalid/012_scalar_position_sequence_payload/expected-error.yaml -998c8f3ef914d9447a6f565be986c0c8d3459b55b6a131290609d912b709e542 SPEC.md 9c11fd643268e578437b015dc5a4ea6e5a3f290f0303ac04ea4d429768208617 tools/schemalib/__init__.py 9c2c853fb20be48e4ebbfdf70a98e6890df116fd77fd85a6be2b010674afcaab go/conformance/conformance_test.go 9d0d594b86072a88bbda400992cb62d0b4faf71176a76c8f30c2e632c8eb5fb1 conformance/validation/011_more_than_one_document/meta.yaml 9da86d5094602943eeb486dae2ad45dec5f918b62db8d1783fb70a3bad3223d2 tests/test_tools/test_infra.yaml 9e78d6bee3ce3d4f6f73b296feb1de14dea12909e82220c8b405b359d0c0be84 conformance/materialization/001_origin_yaml/input.yaml 9e78d6bee3ce3d4f6f73b296feb1de14dea12909e82220c8b405b359d0c0be84 conformance/materialization/013_access_inherit_injection/input.yaml -9e8da6ff0cf2e6a54c8df92c1dce1cf7d66b0a4d22115ea41906b5055c00b058 docs/spec-vector-map.md 9ea1b819377debe7ec47bc8d487a104f5bb991608c4863cda24060264506711d go/objectmodel/schema.go 9f61f29a58d467eb8eb1c40fedb3b5e46342b19eed91fd4fd7233d7da83f510d conformance/invalid/005_origin_in_authoring_input/meta.yaml a03b3fccfc1f8eb7f76833fdff8a50ea84c80c039db87bc0e41109e96f64420b docs/en/concept/declarative_ecosystem_integration.md @@ -238,6 +238,7 @@ aa44b2b55d2ced16b5fc9072a97f2a0c0c30a34afd4055b8ef95591e77e02bfd reviews/82c05a aa44b2b55d2ced16b5fc9072a97f2a0c0c30a34afd4055b8ef95591e77e02bfd reviews/cbaf928.md aad20e1630b166e5ca74baad558d0f47cc9be1d6b6aea22c23deebeaa75447fc conformance/materialization/007_normalize_scalar/expected.yaml ac2343c803409938f2414e5833bab1ae33061506fe2678307121837036e3ada2 conformance/invalid/003_closure_undeclared/schema.yaml +aeb4d33e7a96a9e51e9389eeaa14abf6b663d6dc8eb40c3991c58cc480a7e3ef tools/check_status_claims.py af1074d4241e3ac01c7b4197ffcef9e270dbde4c4ddccd7b1d6c0aab4a75cbb0 docs/hu/architecture.md af6cd3c7db5089246c1c7533fb2763351d03dab7c26424a8827e347fdaee618e tools/schemalib/artifact.yaml b1f4c81b89b74764e424b6f963615944c64b5c11d3e61ef8027334836a51b18d README.md @@ -254,7 +255,6 @@ b75db4a2182e6ebd8b246deedb6af40900dbc24520eaaec02c4738e9dc98d47a reviews/cbaf92 b78df50dc6d7807172273a4ded207fc85511901446f95b472e4ab26c389b4be7 conformance/validation/002_origin_empty/meta.yaml b7c2102b45f5be699a817f2b0e5010d03dddfb67bb74a471994dcb0c70d1e167 spec/origin.schema.yaml b7cc2d3d36c6fef33a7eaa1d13b35465ab293f7c22b6edeb1a86dd2ca834bc87 go/objectmodel/fuzz_test.go -b92e00b843b3423caab27477ac02305bdd077a1c454a5f0dd142e0a90e8188a4 mk/ci.mk b9325f58a95216b240f3715a773e34fe8cdfa4d5fe48a5e7b32002a40be5cc2e tests/test_tools/test_finalize_release.py ba6a2c483d594433ee00f2cc64eec463a68ef2e47babdef5103dff597e3e1773 go/objectmodel/origin.go ba714d37d58ba80b5a99fe675c9dc1f6ace9fc48a2031846f90e1641f78faef1 conformance/invalid/005_origin_in_authoring_input/input.yaml @@ -263,6 +263,7 @@ ba894dcea22bdb7388f28a7a403889ded5d340f436e43c961b2ac5ee5f29ba21 go/module/modu ba8c9ca30ac6149c8a61b177427f2cb4706da1a44ea3d4071695c5cd41aa660d conformance/invalid/009_yaml_alias/expected-error.yaml bab7c71b85a9603d68c7d7b34086a771f1e59452f0d8e02de38c0b8aa27d929b conformance/validation/003_origin_not_terminal/meta.yaml bb7dfd3690714b26ace46ee1f6147feac7c62758f6c4046a10b2cc545e217abc go/module/boundary_test.yaml +bc483249fa73689499646bded7e0e26ff17f2528e93ba1570b292053a5f5f824 docs/spec-vector-map.md bebbdc2c67e4aba1ca6dbb65f8673ea4e443ad986c94f85346d5bcebfbe4db8c reviews/cbaf928.semantic.md bfccbd3cd778ac6f0458b7d45d7b25e2cbdc0a789f40e81c1de476f8aab44363 tools/schemalib/loader.py c077fdb2c25a5db12197280a20de96a1a5b7d59697f26275e0569bd9edf71cbd mk/rust.mk @@ -293,7 +294,6 @@ d75063538a29c37caad5e4f1ba680c81bc4827ee935e78281ab168b72d5e53a2 tools/check_do d807555962a3e6a0fb6d885537d89e3e93c864e0452511fa4aa8b1c3a6231ff9 conformance/invalid/009_yaml_alias/input.yaml d84bf2b93671212d5ef27baed6eff3a8c7c5431273967d1be762b7141696eccc conformance/validation/005_default_member/expected-error.yaml d90f607cbe23c9f809c121670f33e6d023db0a65f8ce9d7bbf33e2d35655727c go/go.mod -db8324d0f397a43b44fb910586ad1f767ff5056709609a120d4b635b25f2ca67 tools/check_status_claims.py dc1bbc3a438b44ae3cc67725dfa916811d24670d4649211987f973aed48b378a go/objectmodel/doc.go df1a1d134a0917f492882af78775b072430c47dd43008625140ad35e206457e9 tests/test_tools/test_releaselib/test_vault_service.py e0841977943dbbd886d89bdd7b3fdf9a91405608d2a2060453669919c4679d31 go/objectmodel/primitives.go @@ -328,12 +328,14 @@ f1d53defce5812b83a7097db81af39bc0b04060d86ddf6e832d21227a793c107 conformance/in f4c4ae3c4c072dc4f44ca1998323ca7ce27b8473e48f1f328f3e3faf143a5af6 tests/infra/__init__.yaml f50c73798e9bb5c4920e78fd0b0b099bed9661f841a3092aebd4f55fa197638b conformance/materialization/013_access_inherit_injection/expected.yaml f5d91d616d254df0e7f6500bc5dff95b2e2f33a66f20a7401d8f1510f5c6bac1 tools/release.sh +f5f675bf64a455bbde4c32a1dbce7824969701af111ea1120e64541dae4bff3f Makefile f67767777225305bf92fe4eb14028b1c9270ccb0699c11ddb57a514108ae3088 tools/releaselib/exceptions.yaml f7a9d92103841ec46c20442f7efc6e14b42212aa16098e851bc471d2d6626a95 conformance/materialization/008_normalize_list/input.yaml f8a48b6ca4c4744cffd03edb72748831df9b83822989bba2bf72c5665466ea1b conformance/invalid/002_sealed_yaml_schema_conflict/meta.yaml f8d5eb9a78aad0da576ad083f7345da57620f3ca0c08d317727e353b44e4da49 go/objectmodel/doc.yaml fa3d02b5fc0677eb715d676e88b98b94016a76eb13e47abfa1040d0e6f12719f docs/rust-gate-extraction.md fa8afcf490e0b9b1e3ce94789f3233ed73289553ec94f16d2c347afd5e08b367 conformance/invalid/002_sealed_yaml_schema_conflict/input.yaml +fa97f776c7e1ed99ffd6fd545462fc64f029ae0f3182386889f958152bee60eb tools/check_review_ledger.py fab14c32130c2345945fa55f41e6614bc443815733249fce81ed7d97e1d1f4cf rust/src/value.rs fd4ba258680da20f4084db2696fa942c09e47de8ec4650104f2b2e18c53980bc go/module/adversarial_test.go ff005c27b6c185b065c2121b01fa6f71c9668af920483714c9b137e411cec32c go/objectmodel/emit_test.go diff --git a/Makefile b/Makefile index b2d574d..a93f224 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ include mk/ci.mk -include mk/rust.mk # ---- Phony ---- -.PHONY: verify verify.fuzz verify.mutate release.subject release.verify review.check status-claims all help validate release test up down shell build fmt lint check typecheck repo.init manifest-verify manifest-update docs.link-check conformance +.PHONY: verify verify.fuzz verify.mutate release.subject release.verify review.check status-claims review-ledger all help validate release test up down shell build fmt lint check typecheck repo.init manifest-verify manifest-update docs.link-check conformance # Default to showing help all: help @@ -167,6 +167,10 @@ manifest-update: ##manifest-update # Documentation # ============================================================================= +review-ledger: ## Verify every review finding is accounted for (INV-046) + @echo "--- Review ledger ---" + @docker compose exec -T builder python tools/check_review_ledger.py + status-claims: ## Verify the documentation's counts and status match the tree @echo "--- Status claims ---" @docker compose exec -T builder python tools/check_status_claims.py @@ -188,7 +192,7 @@ conformance: ## Run the conformance corpus against every present implementation @echo "--- Running conformance vectors ---" @ran=0; \ if [ -f go/go.mod ]; then $(MAKE) golang.test && ran=1; fi; \ - if [ -f rust/Cargo.toml ] && [ -f mk/rust.mk ]; then $(MAKE) test-rust && ran=1; fi; \ + if [ -f rust/Cargo.toml ] && [ -f mk/rust.mk ]; then $(MAKE) rust.test && ran=1; fi; \ if [ "$$ran" -eq 0 ]; then \ echo "NO IMPLEMENTATION PRESENT — 0 vectors executed."; \ echo "The vector corpus exists but is unverified until go/ or rust/ lands."; \ diff --git a/SPEC.md b/SPEC.md index 0bf835d..4ab1874 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1206,7 +1206,7 @@ The procedure, and the three commissioning prompts a request carries, are in ## 13. Related documents - [`docs/pending-decisions.md`](docs/pending-decisions.md) — questions this - document does not answer, which an implementation has answered anyway. Twelve + document does not answer, which an implementation has answered anyway. Fourteen of them, three answered differently in the two implementations. Read this before adding a rule: several of these are load-bearing and unwritten. - [`docs/spec-defects.md`](docs/spec-defects.md) — where this document is not diff --git a/docs/pending-decisions.md b/docs/pending-decisions.md index 453b7c6..ef3cb9b 100644 --- a/docs/pending-decisions.md +++ b/docs/pending-decisions.md @@ -21,8 +21,48 @@ what remains is here. --- +## Where the trust boundary is + +Three of the decisions below turn on this, and it was not written down +anywhere, so it is written down here first. + +`Materialize` **is** the pre-entry step. It validates authoring input against +the schema descriptor and injects the declared defaults, marked — the mark is +`origin`: `[schema]` for a value the model supplied, `[yaml]` for one the author +wrote. What crosses the module boundary afterwards is the complete extracted +set, which is what INV-031's seven prohibitions describe. + +Two consequences that read the wrong way without it: + +- **`Materialize` and `ValidateCanonicalDocument` are the untrusted edge.** The + schema validation is not something that happened before them; it is what they + are. So the alias bomb and the quadratic scan were real threats at exactly + that point, and resource budgets belong there — not at the module boundary, + where the set is already complete. +- **A module never sees an incomplete object.** So "a declared position with no + value" is not a state the boundary can be in. It is a pipeline failure, and + D-2 follows from that rather than from a preference for strictness. + +--- + ## D-1 — Does `scalar_type` constrain a value, or describe it? +> **DECIDED: constraining, with no coercion.** Part of D-5 rather than separate +> work, and a prerequisite for it. +> +> Not for symmetry. A contract cannot be evaluated without a type: `range: [1, +> 100]` against the string `"50"` has no answer. If `scalar_type` stays +> descriptive, the contract evaluator invents its own typing and the model has +> TWO type systems — one declared and unchecked, one implicit — and when they +> disagree the rejection arrives at the wrong stage naming the wrong thing. +> +> No coercion: `9000` is an integer, `"9000"` is a string, and +> `scalar_type: integer` refuses the second. Silent coercion in a provenance +> model is the same failure class as leaving `on`/`off` unquoted. +> +> Sequencing: `scalar_type` enforcement lands BEFORE contract evaluation. The +> other order does not build. + **Measured:** neither implementation reads `scalar_type` during validation. It is parsed, carried, and emitted into the `shape` primitive, and nothing compares it to the payload. `scalar_type: integer` with a string value materializes and @@ -44,6 +84,28 @@ question in an emitter is how a specification acquires rules nobody wrote. ## D-2 — What happens to a declared position that is absent, optional, and has no default? +> **DECIDED: rejection.** Every declared position has a value or a default. +> +> This is not a preference for strictness; it follows from the trust boundary +> above. A complete extracted set crosses into a module, so "a declared position +> with no value" is not a state that can exist there. Either the pipeline +> produces a value or it cannot produce the set, and the second is an error. +> +> **And `required` keeps a meaning, a sharper one.** It stops being about +> whether a value exists on the output — one always does — and becomes about +> where it may come from: +> +> `required: true` = the INSTANCE must supply it; a schema default does not +> satisfy it. +> +> Which is expressible in the object itself: a required member may not carry +> `origin: [schema]`. The keyword is not removed. +> +> Refused: **omit** (Go's behaviour) leaves a reader unable to tell "not +> configured" from "not declared" without the schema. **Null** (Rust's) makes +> every optional position a node holding a value nobody wrote, and `origin` has +> no term that is true of it. + **Measured:** Go removes the node. Rust materializes a scalar as `null`, a list as empty, and walks object children. `docs/spec-defects.md` SD-007 already records three readings — omit, null, reject — and calls none of them decided. @@ -104,6 +166,22 @@ authored a value; the author did not write this one. ## D-5 — Is `access` a grammar or a shape? +> **DECIDED: enforce, and enforce `contract` with it.** The document argued +> these are one question; they are answered as one. +> +> `access` gets a closed member set and typed values in §6.4. `contract` gets an +> evaluator — the primitive §8.7 already requires to be enforced and which +> nothing has ever read. This is the largest single piece of work in the +> repository: a grammar, an evaluator, both implementations, and a vector family +> for each. +> +> Refused: declaring both unenforced was cheap and honest, and would have left +> two primitives that describe guarantees the model does not provide. The +> current state — a MUST in the specification with no code behind it — is worse +> than either, because it looks like a guarantee. +> +> Depends on D-1, which is the type foundation a contract evaluates against. + **Measured:** the operations are checked (`read`, `modify`, and `write` is refused). Nothing else is: `inherit` may be any value, an operation block may carry any member, and `rules` may be anything at all. @@ -126,6 +204,26 @@ checks". They should be decided together or the answer will not be consistent. ## D-6 — What characters may a node name contain? +> **DECIDED: restrict declared names; opaque payload keys are exempt and not +> addressable.** +> +> Schema-declared names take a declared character set, and a schema declaring +> `a.b` becomes a schema-load rejection. Because names arrive from a compiled, +> validated schema descriptor, the restriction is enforceable upstream too and +> the model only has to state it. +> +> An opaque payload's keys are chosen by a foreign system, not by the schema, +> so they stay unrestricted — and §2.5 will say that INV-040 covers the +> MODEL's nodes, not the keys of data carried verbatim. Nothing references into +> an opaque blob: not evidence, not policy. That is what makes the exemption +> free rather than a hole. +> +> Refused: **escape in the address grammar** keeps every name addressable and +> costs a second byte-level rule to pin and reconcile across two +> implementations — §8.8.1 took a day. **Restricting opaque keys too** would +> make §4's "carried verbatim" false for a config blob the model does not even +> interpret. + **Measured:** a child named `a.b` materializes. `Path()` returns `$.values.a.b`, and `Get` splits it into two segments, so the address does not round-trip. Key quoting was fixed for serialization; addressing was not, and the @@ -269,6 +367,76 @@ for a different kind of repository. --- +## D-13 — Is `make ci` the gate, or part of it? + +**Measured:** `README.md` says a green badge and a green local run mean the same +thing. They do not. CI adds `review.check` for pull requests into `main`, which +`make ci` deliberately does not run. And the environment is not fixed: +`Dockerfile` builds `FROM python:3.11-slim` — a moving tag — installs apt +packages without versions, and downloads the Go toolchain over the network with +no checksum in the repository. `mk/rust.mk` installs `cargo-llvm-cov` and +`cargo-deny` unpinned at run time. The Rust base image is the one thing pinned +by digest. + +**Accident on both halves.** The review step was added deliberately and the +README sentence was not revisited; the base image and toolchain were inherited. + +| option | cost | +|---|---| +| **Say "shared core gate"** and add a local target that runs what a main-PR runs | free, and honest; the equivalence claim shrinks to what is true | +| **Pin everything** — digest the Python base, version the apt set, checksum the Go tarball, pin the cargo tools | a reproducible build; every pin becomes something to update | +| **Both** | the only combination under which "the same pipeline" is a statement about bytes rather than about command names | + +**Blocks:** any argument that a green CI run is evidence about a specific tree +rather than about a tree built from whatever the network served that day. +**Related:** audit claim F-08. + +--- + +## D-14 — What does `TestVersionIdentity` promise? + +**Measured:** `README.md` says it holds *every other declaration* in the +repository to `SPEC.md`. It checks two Go constants, two fields of +`spec/index.yaml`, the major and minor of `project.yaml`, and every vector +schema. It contains **zero** references to `rust/`, which has its own +`MODEL_VERSION` and its own `Cargo.toml` version. Those agree today; nothing +makes them. + +**Accident**, and a familiar one: the test was written when there was one +implementation, and its claim was true then. + +| option | cost | +|---|---| +| **Enumerate the Rust declarations too** | one edit; the list grows by hand every time a declaration site appears, which is the failure mode the test exists to prevent, one level up | +| **Discover declaration sites** — scan for a registered pattern across the tree | the claim becomes true rather than maintained; needs a convention for what counts as a declaration | +| **Narrow the README** — name what the test checks | free, and leaves the Rust side unguarded by design rather than by oversight | + +**Related:** audit claim F-10. + +--- + +## Status + +**Four decided, ten open.** + +| decided | choice | +|---|---| +| D-1 | `scalar_type` constrains, no coercion — part of D-5 | +| D-2 | rejection; `required: true` means the instance must supply it | +| D-5 | enforce `access` AND `contract` | +| D-6 | restrict declared names; opaque keys exempt and not addressable | + +D-1, D-2 and D-6 are cheap to implement and change both implementations. D-5 is +a sprint: a grammar, a contract evaluator, two implementations, two vector +families — and D-1 has to land first, because a contract is evaluated against +a type. + +Recommendations exist for the ten still open and are not decisions. D-4 +(`[schema]` for an injected default), D-8 (make the stages and codes normative), +D-12 (replace the release path) and D-13 (pin the build, narrow the `make ci` +claim) look to me like they have one defensible answer each; D-3 follows from +D-5 once the grammar exists; D-7, D-9, D-10, D-11 and D-14 need choosing. + ## How these get decided Not by me and not by an implementation. Each one is settled by an entry in diff --git a/docs/spec-vector-map.md b/docs/spec-vector-map.md index 1ec6043..2dd207f 100644 --- a/docs/spec-vector-map.md +++ b/docs/spec-vector-map.md @@ -65,8 +65,13 @@ for months and "nothing else is" never was. | INV-043 | The canonical serialization, byte for byte | 8.8.1 | every `materialization/*` vector: the runners compare bytes
`materialization/014_hostile_keys`
`validation/011_more_than_one_document` | | INV-044 | Canonical member order | 8.8.2 | `materialization/006_closure_opaque`
`materialization/012_discriminator_payload_keywords`
`materialization/013_access_inherit_injection` | -**32 of 34 invariants are vector-covered.** The remaining -2 are below. +**40 of the 46 invariants are vector-covered.** 7 are declared unvectorizable +with a reason each, below; one invariant appears in both lists because part of +it is vectorizable and part is not. + +These numbers are checked by `tools/check_status_claims.py`. They were "32 of +34" for months after they stopped being true, because a count copied into prose +is a claim with no owner. ## Unvectorizable @@ -77,7 +82,7 @@ a check of a different sort rather than being silently left uncovered. | Invariant | Why no vector | How it is checked instead | |---|---|---| -| INV-031 | The seven prohibitions (a–g) are properties of what crosses a module boundary, and this repository contains no module and no boundary. Each clause is nonetheless enforced *upstream* by an existing vector — the pipeline eliminates the condition before a module could observe it — so the risk is not that a clause is unenforced but that no single test names the boundary itself. | Clause-by-clause upstream coverage: (a) unresolved references — `invalid/003_closure_undeclared`; (b) short forms — `materialization/007_normalize_scalar`, `008`, `009`; (c) templates — `materialization/003_origin_sealed`, `004`; (d) sealed fragments — `invalid/001_sealed_yaml_conflict`; (e) unapplied defaults — `materialization/002_origin_schema`, `010`; (f) unknown primitives — `invalid/006_unknown_primitive`; (g) unvalidated objects — all `validation/*`. A true boundary test is deferred to the sub-jobs, which must add one integration test per clause. | +| INV-031 | The seven prohibitions (a–g) are properties of what crosses a module boundary. A vector is YAML in, YAML out and cannot construct a boundary crossing, so the clauses are enforced *upstream* — the pipeline eliminates each condition before a module could observe it. | Clause-by-clause upstream coverage: (a) unresolved references — `invalid/003_closure_undeclared`; (b) short forms — `materialization/007_normalize_scalar`, `008`, `009`; (c) templates — `materialization/003_origin_sealed`, `004`; (d) sealed fragments — `invalid/001_sealed_yaml_conflict`; (e) unapplied defaults — `materialization/002_origin_schema`, `010`; (f) unknown primitives — `invalid/006_unknown_primitive`; (g) unvalidated objects — all `validation/*`. **The boundary itself is tested**: `go/module/boundary_test.go` asserts INV-031 clause by clause, and `go/module/adversarial_test.go` attacks it — that is where the split-view and counting forgeries were built. This entry said "this repository contains no module and no boundary" and "a true boundary test is deferred to the sub-jobs" long after `go/module/` existed and both files were written. | | INV-005 | 0.2 terminates on schema finiteness, and the schema language is a finite literal tree with no references (§8.2 is out of scope). A schema that cannot be walked to a leaf **cannot be written** in it, so no vector can violate the invariant. 0.1 hid this by testing a different and stronger property — name-acyclicity — which the corpus could express; see `spec-defects.md` SD-005. | Nothing to check today, and that is the honest state. When the reference syntax lands (§8.2), finiteness stops being structural and this invariant becomes both violable and vectorizable. INV-005's forward note in §2.3 is the reminder. | | INV-045 | The conformance corpus is YAML in, YAML out: a vector states what the MODEL does with a document. A release is a property of the repository — which files exist and what they hash to — and there is no authoring input that can violate it. A vector could only test it by containing a copy of the tree it is about. | `make release.verify`, which recomputes the subject from the tree and compares it to the descriptor's claim, and `tests/test_tools/test_release_subject.py`, which builds a temporary repository and asserts that changing one byte of the specification, of a vector, or of an implementation breaks the subject. Run in `ci.gates`, so a tree whose descriptor has gone stale fails before anything else is checked. | | INV-046 | A review is a person looking at a tree. No authoring input can violate it, and no vector can establish that someone examined something — a corpus can only state what the MODEL does with a document. | `make review.check`, which requires a record in `reviews/` named for the tree's subject digest, run by CI on every pull request into `main`. `tests/test_tools/test_release_subject.py` asserts the binding: a record for a different tree does not count, and one byte of change to the specification makes an existing record stop applying. What no check establishes — that a person did the work, or did it well — is stated as not claimed rather than implied by a green line. | diff --git a/mk/ci.mk b/mk/ci.mk index 05640e6..24c5bb9 100644 --- a/mk/ci.mk +++ b/mk/ci.mk @@ -62,7 +62,7 @@ ci.deps-drift: # ci.gates is everything that holds for this repository whether or not an # implementation exists: integrity, links, code quality, security. -ci.gates: manifest-verify release.verify status-claims docs.link-check check ci.security +ci.gates: manifest-verify release.verify status-claims review-ledger docs.link-check check ci.security # ci.security runs the scanners as their own step rather than hiding inside # `check`. A security finding should be legible as a security finding. diff --git a/project.yaml b/project.yaml index b11451e..2a7201f 100644 --- a/project.yaml +++ b/project.yaml @@ -49,7 +49,7 @@ metadata: # # Recompute with `make release.subject`; check with `make release.verify`, # which a third party can run with a clone and a Python and nothing else. - buildHash: '90f232d2fede1efd83bbdd4b7e6e5928030edaa9b67adf6d67fe300a405ad4c8' + buildHash: 'd28fb422fd0d730dea6054a5308e3cf152e5ab6bdeb2b42df341bdcfd48b402f' cicSign: 'TBD' cicSignedCA: certificate: "TBD — filled by the release process with the CIC Root CA certificate" diff --git a/reviews/README.md b/reviews/README.md index f6eb18a..aacd75d 100644 --- a/reviews/README.md +++ b/reviews/README.md @@ -33,6 +33,22 @@ saw it — a false positive in the test written to check this gate. An external audit found it by computing both digests. Nothing in the repository did. +## The disposition ledger + +`.disposition.md` carries one row per finding: closed, partial, or +open, and where. `make review-ledger` reconciles it against the findings the +review threads raise and the decisions in +[`../docs/pending-decisions.md`](../docs/pending-decisions.md), and fails when +any of the three disagree. + +It exists because the first attempt lost two. The decisions document listed +twelve; `claim/F-08` and `claim/F-10` had been raised, measured and confirmed, +and then appeared in no decision and no commit. Nothing compared the list to its +source, so nothing could say so. + +A `partial` row is not a closed one. The count that matters for "is this review +discharged" counts only `closed`. + ## What is here Records from the first commissioned review, run as three separate threads diff --git a/reviews/cbaf928.disposition.md b/reviews/cbaf928.disposition.md new file mode 100644 index 0000000..4736985 --- /dev/null +++ b/reviews/cbaf928.disposition.md @@ -0,0 +1,64 @@ +# Disposition — review of `cbaf928` + +One row per finding of the three review threads. `tools/check_review_ledger.py` +requires that every finding raised in `cbaf928.{claim,semantic,adversarial}.md` +appears here exactly once, and that every row not marked `closed` names a +decision that exists in [`../docs/pending-decisions.md`](../docs/pending-decisions.md). + +That gate exists because this ledger's first version lost two findings. The +decisions document listed twelve; `claim/F-08` and `claim/F-10` were in neither +it nor any commit, and the omission was invisible because nothing compared the +list to its source. Which is the failure this repository has now found in the +manifest, in the status claims, in the vector map, and here — a list that looks +complete because nothing checks it against what it is a list of. + +**Status vocabulary** + +- `closed` — the finding no longer reproduces; the commit is named. +- `partial` — the executable half is fixed and the specification half is a + decision. Both halves are named. A `partial` row is NOT closed, and the gate + treats it as open. +- `open` — untouched, or a decision only. + +| finding | status | where | +|---|---|---| +| adversarial/F-01 | closed | Rust `required` enforced — PR #17 | +| adversarial/F-02 | closed | list position rejects a non-sequence — PR #17 | +| adversarial/F-03 | closed | scalar position rejects a collection, both — PR #17 | +| adversarial/F-04 | closed | keys quoted; Go reparses its output — PR #18 | +| adversarial/F-05 | closed | a primitive member must be a node — PR #20 | +| adversarial/F-06 | closed | more than one document is refused, both — PR #20 | +| adversarial/F-07 | closed | boundary reads each method once, `Delivered` — PR #16 | +| adversarial/F-08 | closed | scan and map insert de-quadratised — PR #22 | +| adversarial/extra-origin-term | closed | sealed term is exactly two scalars — PR #22 | +| adversarial/extra-nonstring-keys | closed | Go rejects non-string keys — PR #22 | +| adversarial/budgets | open | D-11 | +| claim/F-01 | closed | `reviews/` excluded from the subject — PR #15 | +| claim/F-02 | partial | schema accepts `repo_type: spec` (PR #22); the release path still does not compute the subject — D-12 | +| claim/F-03 | partial | four divergences closed with vectors (PR #17, #20, #22, #23); the general equivalence claim remains bounded by the corpus — D-2 | +| claim/F-04 | partial | serialization quoting closed (PR #18); addressing for names containing `.` — D-6 | +| claim/F-05 | open | D-9 | +| claim/F-06 | partial | counts corrected and gated (PR #21, #24); the gate reads `INV-nnn` and not RFC-2119 clauses — D-10 | +| claim/F-07 | closed | status-claims gate — PR #21, #24 | +| claim/F-08 | open | D-13 | +| claim/F-09 | closed | release docs replaced — PR #22 | +| claim/F-10 | open | D-14 | +| claim/F-11 | partial | arity enforced (PR #17); `scalar_type` — D-1 | +| semantic/F-01 | open | D-3 | +| semantic/F-02 | open | D-4 | +| semantic/F-03 | open | D-5 | +| semantic/F-04 | open | D-1 | +| semantic/F-05 | open | D-2 | +| semantic/F-06 | partial | nested expansion and innermost origin pinned by `materialization/015` (PR #23); adoption semantics unwritten — D-3 | +| semantic/F-07 | partial | 64-level bound in both (PR #23); §2.3's finiteness argument still assumes no references — D-7 | +| semantic/F-08 | partial | term shape tightened (PR #22); the machine schema and the validators have not been compared — D-8 | +| semantic/F-09 | open | D-6 | +| semantic/F-10 | partial | keys closed (PR #18); the exact decimal form of a float is unspecified — D-1 | +| semantic/F-11 | partial | anchors, aliases, duplicate and non-string keys refused (PR #20, #22); tags, `!!binary`, version directives — D-7 | +| semantic/F-12 | open | D-8 | +| semantic/F-13 | open | D-8 | +| semantic/F-14 | open | D-9 | +| semantic/F-15 | closed | status-claims gate — PR #21, #24 | + +**Totals: 14 closed, 10 partial, 13 open.** A partial is not a closed one; the +count that matters for "is this review discharged" is 14 of 37. diff --git a/tools/check_review_ledger.py b/tools/check_review_ledger.py new file mode 100644 index 0000000..bb09242 --- /dev/null +++ b/tools/check_review_ledger.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +"""Every review finding is accounted for, and every open one names a decision. + +A review produces findings; a repository closes some and defers others. The +deferral is the dangerous half: a finding that is neither fixed nor written down +has not been decided against, it has been forgotten, and nothing about the tree +looks different afterwards. + +This reconciles three lists that are supposed to describe the same set: + + the findings raised in reviews/.{thread}.md + the rows of reviews/.disposition.md + the decisions in docs/pending-decisions.md + +and fails when they disagree. Specifically: + + * a finding with no ledger row — raised and then lost + * a ledger row for a finding nobody raised — a row that outlived its finding + * an open or partial row naming a decision that does not exist + * a decision nothing refers to — either the finding it came from was closed + without updating the row, or the decision was invented + +The last two are what happened here. The decisions document listed twelve; +`claim/F-08` and `claim/F-10` had been raised, measured, confirmed, and then +appeared in no decision and no commit. Nothing compared the list to its source, +so nothing could say so — the same shape as the manifest that verified only the +files it listed, and the status claims that were true when written. +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +THREADS = ("claim", "semantic", "adversarial") + + +def repo_root() -> Path: + return Path(__file__).resolve().parent.parent + + +def subjects(reviews: Path) -> list[str]: + """Every subject that has a disposition ledger.""" + return sorted( + p.name[: -len(".disposition.md")] for p in reviews.glob("*.disposition.md") + ) + + +def raised(reviews: Path, subject: str) -> set[str]: + """Findings the review threads raise, namespaced by thread. + + Namespaced because the threads number independently: `claim/F-01` and + `adversarial/F-01` are different findings, and a ledger keyed on `F-01` + alone would silently hold one row for two. + """ + out: set[str] = set() + for thread in THREADS: + path = reviews / f"{subject}.{thread}.md" + if not path.is_file(): + continue + for fid in re.findall( + r"^#{2,3} (F-\d+)", path.read_text(encoding="utf-8"), re.M + ): + out.add(f"{thread}/{fid}") + return out + + +def ledger(path: Path) -> dict[str, tuple[str, str]]: + """finding -> (status, where), read from the disposition table.""" + rows: dict[str, tuple[str, str]] = {} + for line in path.read_text(encoding="utf-8").splitlines(): + m = re.match( + r"^\| ([a-z]+/[A-Za-z0-9-]+) \| (closed|partial|open) \| (.*?) \|$", line + ) + if m: + rows[m.group(1)] = (m.group(2), m.group(3)) + return rows + + +def decisions(path: Path) -> set[str]: + return set(re.findall(r"^## (D-\d+) —", path.read_text(encoding="utf-8"), re.M)) + + +def main() -> int: + root = repo_root() + reviews = root / "reviews" + pending = root / "docs" / "pending-decisions.md" + known = decisions(pending) + failures: list[str] = [] + referenced: set[str] = set() + totals = {"closed": 0, "partial": 0, "open": 0} + + found_any = False + for subject in subjects(reviews): + found_any = True + rows = ledger(reviews / f"{subject}.disposition.md") + raised_here = raised(reviews, subject) + + # Findings raised outside the numbered headings — a section rather than + # an F-nn — are carried in the ledger under a descriptive id. They + # cannot be discovered automatically, so they are allowed to exist in + # the ledger without a heading, and that indulgence is stated rather + # than silent. + extra_ok = {k for k in rows if not re.match(r"^[a-z]+/F-\d+$", k)} + + for fid in sorted(raised_here - set(rows)): + failures.append(f"{subject}: {fid} was raised and has no ledger row") + for fid in sorted(set(rows) - raised_here - extra_ok): + failures.append( + f"{subject}: ledger has a row for {fid}, which no thread raises" + ) + + for fid, (status, where) in sorted(rows.items()): + totals[status] += 1 + if status == "closed": + continue + named = set(re.findall(r"\bD-\d+\b", where)) + if not named: + failures.append(f"{subject}: {fid} is {status} and names no decision") + continue + for d in named: + referenced.add(d) + if d not in known: + failures.append( + f"{subject}: {fid} names {d}, which docs/pending-decisions.md does not define" + ) + + if not found_any: + print("no disposition ledger found; nothing to reconcile") + return 0 + + for d in sorted(known - referenced): + failures.append( + f"docs/pending-decisions.md defines {d}, which no open finding refers to" + ) + + print( + f"findings: {totals['closed']} closed, {totals['partial']} partial, " + f"{totals['open']} open, over {len(known)} decisions" + ) + print() + + if failures: + print("FAIL the review ledger, the findings and the decisions disagree:") + for f in failures: + print(f" {f}") + return 1 + + print( + "ok every finding has a row, and every open row names a decision that exists" + ) + print() + print("A partial is not a closed one. This checks that nothing was lost, not") + print("that anything was solved.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/check_status_claims.py b/tools/check_status_claims.py index d1ccef3..d19c09e 100644 --- a/tools/check_status_claims.py +++ b/tools/check_status_claims.py @@ -55,6 +55,19 @@ def measured(root: Path) -> dict[str, int]: vectors = len(list((root / "conformance").glob("*/*/meta.yaml"))) + # How many invariants a VECTOR claims, which is the definition + # check_spec_vectors.py uses when it reports coverage. Counting the map's + # table instead gave a different number for the same fact — two tools + # disagreeing about one thing, which is the defect this file exists to + # catch, committed while catching it. + covered = set() + for meta in (root / "conformance").glob("*/*/meta.yaml"): + text = meta.read_text(encoding="utf-8") + block = re.search(r"^invariants:\s*\[([^\]]*)\]", text, re.M) + if block: + covered.update(re.findall(r"INV-\d+", block.group(1))) + covered &= set(re.findall(r"^\| (INV-\d+) \|", index, re.M)) + # An implementation counts when it has BOTH sources and a conformance # runner. A directory with a .gitkeep is not an implementation, and one # that cannot run the corpus is not evidence about the model. @@ -66,14 +79,14 @@ def measured(root: Path) -> dict[str, int]: has_sources = any((root / lang).rglob("*.go")) or any( (root / lang).rglob("*.rs") ) - has_runner = (root / runner).exists() - if has_sources and has_runner: + if has_sources and (root / runner).exists(): implementations += 1 return { "invariants": invariants, "vectors": vectors, "implementations": implementations, + "covered": len(covered), } @@ -88,6 +101,18 @@ def measured(root: Path) -> dict[str, int]: ("README.md", r"pass all (\d+) vectors", "vectors"), ("docs/en/architecture.md", r"SPEC\.md — (\d+) numbered invariants", "invariants"), ("docs/en/architecture.md", r"conformance/ — (\d+) vectors", "vectors"), + # The sentence that said "32 of 34" for months. Both halves are checked, so + # neither can drift alone. + ( + "docs/spec-vector-map.md", + r"\*\*(\d+) of the \d+ invariants are vector-covered", + "covered", + ), + ( + "docs/spec-vector-map.md", + r"\*\*\d+ of the (\d+) invariants are vector-covered", + "invariants", + ), # docs/hu/architecture.md states no counts, deliberately: it describes the # layers and points at the register. A document that does not make a # numeric claim has nothing here to go stale, which is the cheapest fix