From bda337e3bceb46ec88c6252f7b91782aeccf94b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Z=20Sink=C3=B3?= Date: Sun, 9 Aug 2026 19:59:32 +0200 Subject: [PATCH] fix: a sealed_from chain expands to the template it ultimately names (audit semantic F-06/F-07) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A template entry may itself be sealed from another template. Go expanded the chain; Rust resolved exactly ONE level, and because a `sealed_from` node declares no shape of its own the node then defaulted to object and materialized as `{}`. Two different objects from one schema: go: values: from-inner origin: [{sealed: {template: $inner, path: $.leaf}}, schema] rust: values: {} origin: [{sealed: {template: $outer, path: $.mid}}] The corpus had no nested template — 003 and 004 cover a single level — so neither runner could see it, and the divergence sat behind a claim that the two implementations check each other. What the expansion also settles, and the specification does not state: the origin names the INNERMOST template and path, the place the value actually came from, not the mount point where the chain was entered. Go answered that question by implementation and nothing wrote it down. The vector pins it now; SPEC §5 should say it (audit semantic F-06). Expansion is bounded at 64 levels in both. Go had that bound; Rust had none, which was harmless only because it did not recurse at all. Recursing without a bound would have turned a chain of templates referring to each other into an unbounded loop over input a caller supplies (audit semantic F-07). Measured: a 200-level chain now returns "template expansion exceeded the maximum depth" rather than running. Both implementations produce byte-identical output for the new vector. 38 now. One thing the status-claims gate did while this landed, worth recording because it is the first time it fired for real: adding the vector made docs/en/architecture.md's "37 vectors" false, and `make ci` refused before anything else ran. That is the class of defect it was written for, caught on the commit that created it rather than by an audit months later. --- [signing-metadata] key = cic-my-sign-key signature = vault:v1:MEQCIAk1sTJ3nIGR30UpebXbjIscwc/ReWGCt3rfWBjlGLfvAiBGzDlrMq7ppr62+W+uoIbyUGLLlubZ4168qhmOBIS0aQ== hash-algorithm = sha256 digest = Gc2FvV2yyH4ThVDStMZTrkjd/V+UeBuPeMTuv5BtdwE= [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 +++-- README.md | 4 +- .../015_nested_sealed_template/expected.yaml | 21 ++++++ .../015_nested_sealed_template/input.yaml | 2 + .../015_nested_sealed_template/meta.yaml | 26 +++++++ .../015_nested_sealed_template/schema.yaml | 20 +++++ docs/en/architecture.md | 2 +- docs/spec-vector-map.md | 2 +- go/conformance/conformance_test.go | 5 +- project.yaml | 2 +- rust/src/materialize.rs | 75 ++++++++++++++----- rust/tests/conformance.rs | 2 +- 12 files changed, 146 insertions(+), 33 deletions(-) create mode 100644 conformance/materialization/015_nested_sealed_template/expected.yaml create mode 100644 conformance/materialization/015_nested_sealed_template/input.yaml create mode 100644 conformance/materialization/015_nested_sealed_template/meta.yaml create mode 100644 conformance/materialization/015_nested_sealed_template/schema.yaml diff --git a/MANIFEST.sha256 b/MANIFEST.sha256 index a8b7e32..6f8ed6b 100644 --- a/MANIFEST.sha256 +++ b/MANIFEST.sha256 @@ -9,6 +9,7 @@ 04e67c89d88d0f33d8e898ec3396359f48da372fd7ab28c1fc70483a679b7fe6 conformance/validation/006_model_version_inside_object/object.yaml 052458dca6bed674aec47f25561534ce72cda99a62faf33e2e947474400c7374 tools/vault-rootCA-sign-agent.sh 06b8bd2d88fa048c5ee235eea47af951aa782691635c77ad616dab0abe3a27ed conformance/invalid/003_closure_undeclared/expected-error.yaml +07dea1fcabc03fcec5648b8834202db6d505d625c07cc0d94db40cb13ffae61b conformance/materialization/015_nested_sealed_template/meta.yaml 08565714b7e2064ac0f9c691ed50215f80363adb08f8976589bebdcbb717f54e renovate.json 085f2e3cbd9cb9a6a7bc53a42cfa8be8fa51ca27cadd56edfdd21dda0d34e15b docs/external-review.md 090194d2cbde04322fd68bed8ce5427b4b0ad5daba1b26017958fed8ef29c964 tools/release_subject.py @@ -51,6 +52,7 @@ 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 +25330918bb1de80bf3db79532e39dd87e2752df46e402434c0044de476046f86 README.md 2758d20cf5aa0688060bce28a52786eee272986714a8376d7f190eb2dc90a43e reviews/README.md 275acc4de6f4d5fcd87330039777ceb8f8d8a7933c4e4924eccabe21a9f28da2 conformance/materialization/002_origin_schema/meta.yaml 27fc384a0d7987e4cb311648408b9b5a3995c0b58871ed55389ae154061209c9 conformance/validation/011_more_than_one_document/object.yaml @@ -59,16 +61,13 @@ 2ab20175d0b7d0f2a7ebe960903a08dc7fe69620615f253ae1fa3e584747edaa pytest.ini 2b1268f3aba4ac61ee21ea6a558fb8b01d2ce4964b8993b0e0b0aae583303b08 go/objectmodel/primitives.yaml 2b5c7c2028ce8d8fa9ec385fed586a85d22f30903373b511111011c6e6a34044 go/objectmodel/emit.go -2be51eae75514d81524b44089859b25fa7282a962475cf9bf196b87ebb96cbd9 rust/src/materialize.rs 2bf4423a50b568d1aa9bf938a233f9584e2c6ef19f5dd3ebf458c9a1dd093a30 go/objectmodel/schema.yaml -2cafa2e6c89efdc947b2dc5206041dd3640892c6cdcb8a92f9852eff5f8e9413 docs/spec-vector-map.md 2d28d83b74f3b137a2e2d02d21178ad751b9b59ea81133ec42c9704ca535c986 tools/init_from_template.sh 2d66d76a7399d07f6dc504d64a22e81dfcaa49f6bcee9ab4362c15dcb82b2475 conformance/validation/006_model_version_inside_object/expected-error.yaml 2d763d2e7243d988b6f107432e14f4ac45d10a642ea72abd150ed30d8c3261c2 tools/check_spec_vectors.py 2e19ebdd9db8f56f6dc5fb006f8bb23037595794d0c288e72901b9f628d2d5b5 tools/git_hook_commit-msg.sh 2e83c8f6100c847450f90a1f593db97b7ad2e7cc7798bb762823b61ebcdeff57 go/objectmodel/refs.go 2e85d74791ed8db0bee2e473f3d5906c351c1c962901e12dc4278198a44d07a3 docs/hu/concept/declarative_ecosystem_integration.meta.yaml -2f7e223bf0e58d18a8705d175d0c0845df050c4429e72f22f54eb84e11f4022d docs/en/architecture.md 3057e97eafa5993ad6761b7c1b25076432fc5b1998dc7c4d569926bc37a974b2 tests/test_compiler.yaml 306916d0bc25ffb21038fa8ac7421dddec5dfae2483f3a802d5d0970194ebfbb conformance/materialization/001_origin_yaml/expected.yaml 308a0bc08020a9fe5fc59e2310af16c0aebafcfb3fa9c1b4ac8e585da1967274 conformance/materialization/010_normalize_empty_object/meta.yaml @@ -131,6 +130,7 @@ 60a7ef1423b335eab7c75d37e0be6d1ce517b50d1a7c3fae95ceefae57cc46f3 docs/spec-defects.md 60f0024efc017a5d11dd4a16fd95e8ae01ab3b5a520082ac401b034616ce1f6f go/cmd/cic-materialize/main.go 61abb4260dac215d5b246dfb2459b9fbb3c6edb55c4ca972ea81763524d95f8e rust/deny.toml +62f72bcff9b4da95f5fe2bdd98408f6fdc6bcea0d030b7ef0bde9836869ee540 rust/src/materialize.rs 631a400349d4bdecf455e7f95d400a5bf27e93621c58a9d1886a7f66d55c4f26 conformance/invalid/004_schema_declares_values_child/input.yaml 63beeb3172fdd17b9f232ac1194a364e58b3f66ce63c177f7da877181ab340d9 conformance/invalid/010_required_value_missing/schema.yaml 63c33f2fba73bf59d5e881b0a7c8d6cb958f58b2de0fab22044a8eba50666f2b go/objectmodel/canonical.yaml @@ -138,9 +138,9 @@ 66181e4c6a0b2735b5a8b3088b37e6a844e2c692399a5b72b036059da0188e0a tools/releaselib/__init__.py 66c65cbf1c8a094275f1ec49420f2bed3c3841fca11ddba4080ae62059532aed conformance/materialization/002_origin_schema/expected.yaml 66e962cd93fcc33b1dd43760f78d7de6b7e718923536d814d36ff0da8ce10223 conformance/validation/010_primitive_member_is_not_a_node/meta.yaml +6734fec32b2c899e9e945fb42c40d199da37830a62ff36fa550c43ab717b53d3 rust/tests/conformance.rs 68a4754be7f4971ea34eb5277b054692d6582edece24fa50b279682c69380d2f conformance/materialization/003_origin_sealed/meta.yaml 6a9d33a921b501cfea3f229df86d6f26e6cbbfddfd9dde90d66451d1c25ae5f3 conformance/validation/010_primitive_member_is_not_a_node/object.yaml -6b78afd099dcc9b830c51b1da97ab491374167491f3f3b5c0c34129511f73750 go/conformance/conformance_test.go 6d9c5cce2c2ce964f0add1b4aeb97a9bce828ad46dd76b22929e284741273fc4 schemas/index.yaml 6db64092fb2c2dc8bcbd3e321faf04f6cc40b16e890c8dec3e56e4178df01402 LICENSE.yaml 6dbe5368b6622c9caa171419a4c8efce9c423960df4f45e69176ba4cba201981 go/objectmodel/template.yaml @@ -163,7 +163,6 @@ 76dfd3a8e0b9c8b5b2741e10cdefb64bbf24986a9fc3b448d53e2f7adedeb7f4 go/objectmodel/node.go 7758cf44c1eb621c310a5badf0222395851b1e4186ad143b19dd01e5e570949b conformance/materialization/009_normalize_map/expected.yaml 77eff1b8af60b8c502d01faa46c224449cf7f1d1f3e0d970d9614f22b3478f69 conformance/validation/003_origin_not_terminal/expected-error.yaml -78d6a00f5cb07f9675bce32f27fc0bb565858b203d2867bedb1bd3a5f31300cf project.yaml 78ddec6239d07162d53b5962e195055387d8b936e33c000ebeb75fba1242ef6a conformance/materialization/009_normalize_map/meta.yaml 791e0fac77217234c5d02a0c1e4bbc6e770a4f3b9805fffd013e3669fc81c2fb docs/en/architecture.yaml 7b0579d52d41811c7e6faa50bcebf0bb61084b7babffdc13996740a884aaccfa conformance/invalid/006_unknown_primitive/meta.yaml @@ -195,8 +194,8 @@ 8ac2a6cb77025dfe7eaf732e15887a28e0563a81a444d465a535372ca115cd6e conformance/materialization/002_origin_schema/input.yaml 8ac2a6cb77025dfe7eaf732e15887a28e0563a81a444d465a535372ca115cd6e conformance/materialization/003_origin_sealed/input.yaml 8ac2a6cb77025dfe7eaf732e15887a28e0563a81a444d465a535372ca115cd6e conformance/materialization/004_origin_sealed_schema/input.yaml +8ac2a6cb77025dfe7eaf732e15887a28e0563a81a444d465a535372ca115cd6e conformance/materialization/015_nested_sealed_template/input.yaml 8bc9462d24b8665180327f10566c7f3f963d36ba32792a06ab889bd0a58b245d conformance/validation/004_documentation_member/expected-error.yaml -8c8825a7cbc9348e2605ab6e37f55015df8d7fa485556f6ffbadfbc40aa77280 README.md 8d2c9c9396590aefcb54aaad5c9eccca0a41cc4ecafb5b473dbbb0e8f49c8e3a go/objectmodel/errors.yaml 8d90f4c93ede5d116093856ddc214346e8b4188d7c7fe456ad1b955658ab511e conformance/validation/010_primitive_member_is_not_a_node/expected-error.yaml 8d9adb32decde0cc3abe606560882a6c36cd15f095501d3f4e43ab9c7cfae03c conformance/invalid/001_sealed_yaml_conflict/input.yaml @@ -210,12 +209,15 @@ 93460d0514f94a64d51eff7fa538f78517aca3bafaf58ea15c6b9fc93f33bc7f conformance/validation/002_origin_empty/expected-error.yaml 94905a94361ac37947c3f5c6fb01b55d431a7459e8e8291ff6bbc25042c37fba conformance/materialization/010_normalize_empty_object/expected.yaml 94dabea4d0bdb7a81c750c4b2830f096c9ee2e59952fd1b9f7d4e235cddeb2f9 mk/golang.mk +96a0612d7f3d7d3faf96bbc408da0b32979eaee2599962e8d6d9e97c15b38d40 conformance/materialization/015_nested_sealed_template/schema.yaml 992423396d17250586acefe1ca589c21d650fb75c7f578bdff9cbb42fa1351fa conformance/invalid/012_scalar_position_sequence_payload/expected-error.yaml 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 @@ -242,6 +244,7 @@ b3d0d0dd71c294b5fa8bad4fcdd9784feae35141aeb71e1755abd8bbd0cfca58 pyproject.toml b608bfc35f2bcb3d0bda472b13f3f1480c5e94c6ddeeacf5922c8fa0e5f86b15 conformance/invalid/002_sealed_yaml_schema_conflict/schema.yaml b608bfc35f2bcb3d0bda472b13f3f1480c5e94c6ddeeacf5922c8fa0e5f86b15 conformance/materialization/004_origin_sealed_schema/schema.yaml b614b3b83716f77c3f14216a59bb13c7205554ad802d0aea364c01f0e6c0af67 go/objectmodel/node.yaml +b6940b9faba73a0a5ff208868258c183d11efe9e90b0ff77268598c1f726d00b docs/en/architecture.md b6bda230e34be59f295ef57a8ba98469fcff5fd0a717f111ec4fc317f9de4a9f go/objectmodel/document.go b6c301c0695c34e7304717dcf3aeca7f022a7dec68b5e42303fad3e0d0aa57e1 conformance/invalid/006_unknown_primitive/expected-error.yaml b75db4a2182e6ebd8b246deedb6af40900dbc24520eaaec02c4738e9dc98d47a reviews/cbaf928.claim.md @@ -250,7 +253,6 @@ b7c2102b45f5be699a817f2b0e5010d03dddfb67bb74a471994dcb0c70d1e167 spec/origin.sc b7cc2d3d36c6fef33a7eaa1d13b35465ab293f7c22b6edeb1a86dd2ca834bc87 go/objectmodel/fuzz_test.go b92e00b843b3423caab27477ac02305bdd077a1c454a5f0dd142e0a90e8188a4 mk/ci.mk b9325f58a95216b240f3715a773e34fe8cdfa4d5fe48a5e7b32002a40be5cc2e tests/test_tools/test_finalize_release.py -b9d89d1f2d541eb9d523bc3f9b3939a0bdebabd5558038b86cb3ec19a8370af0 rust/tests/conformance.rs ba6a2c483d594433ee00f2cc64eec463a68ef2e47babdef5103dff597e3e1773 go/objectmodel/origin.go ba714d37d58ba80b5a99fe675c9dc1f6ace9fc48a2031846f90e1641f78faef1 conformance/invalid/005_origin_in_authoring_input/input.yaml ba83e549fe8de95a493a67b30279205fb01be02565d3a508bd2742bd7274be35 tests/test_tools/test_finalize_release.yaml @@ -262,6 +264,7 @@ bebbdc2c67e4aba1ca6dbb65f8673ea4e443ad986c94f85346d5bcebfbe4db8c reviews/cbaf92 bfccbd3cd778ac6f0458b7d45d7b25e2cbdc0a789f40e81c1de476f8aab44363 tools/schemalib/loader.py c077fdb2c25a5db12197280a20de96a1a5b7d59697f26275e0569bd9edf71cbd mk/rust.mk c4471dc9ac37e73e99045561799f7d9b71e284f77144f31cf80e23794032cb5b tests/test_tools/__init__.yaml +c49098a5392223d55ae4b4dcedebbbf3b3af02ba72f7bf1bcf5553f849ff245d conformance/materialization/015_nested_sealed_template/expected.yaml c4bbe4491584f47fe89a58a88f5a7a49deeac1cad1f488cb6c15b7c7a72962b7 conformance/invalid/011_list_position_scalar_payload/expected-error.yaml c54659558693be5238ff020322eb00ca80ce7d14aea461ee1ae24361e80fe84f tools/releaselib/vault_service.py c55c74bb201c91330aa8bda11dd256fe35f84b3a3a0e24a1cebe3e487bd60135 requirements.in @@ -270,6 +273,7 @@ c874c3372e425b2255c6eb1771e13f77fe6025923d06ced7b5a91259e28e1d22 go/objectmodel c8daecc8e4b81241d10c642c6c051712022bb8950a7855fd31d7805534de9586 conformance/materialization/006_closure_opaque/expected.yaml cadf923c73fdea6b9e1478f822f30d3f4d6c7463b15a12df709f0ae5d56743a2 conformance/validation/007_origin_duplicate_term/meta.yaml cc2109e262ef7d2c0ae924af62ae6b794934a37c9be09e35f1a175b22faa2483 go/objectmodel/construct.go +cc22d2735173aaf85a9944852f4522df726185bd7f8b023085fd7d0c5d7fe6d4 project.yaml cc94b0652d20afaf3b85ee72e50913f0277b6441642309670818355847f1e721 spec/node.schema.yaml ccc12224c9262ebed8b7bedb3046e9a359fa817e9c08659765d62d63355d0e3a conformance/materialization/014_hostile_keys/input.yaml cdc268dd275143f3cbad6b84078f397367c40951812143d227b5490cd007ce82 tools/infra.py diff --git a/README.md b/README.md index 7b47266..81b3f7e 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ written into SPEC.md and propagated nowhere else. | Component | Status | |---|---| | `SPEC.md` — 46 numbered invariants | written, normative | -| `conformance/` — 37 vectors | executed against Go on every CI run | +| `conformance/` — 38 vectors | executed against Go on every CI run | | `docs/spec-vector-map.md` | 37 invariants vector-covered, 5 declared unvectorizable with a reason each | | `tools/check_spec_vectors.py` | runs and passes; negative-tested | | `go/` | implemented — corpus, fuzz, mutation, adversarial and CLI golden tests | @@ -26,7 +26,7 @@ written into SPEC.md and propagated nowhere else. | `mk/rust.mk` | present; digest-pinned toolchain, `make rust.quality` | | Docker build / CI | runs; `make ci` is the same pipeline locally and in Actions | -Both implementations pass all 37 vectors, and neither was written from the +Both implementations pass all 38 vectors, and neither was written from the other: the Rust one was written from `SPEC.md` and the corpus, deliberately not from `go/`. Two implementations that share an author's reading share that reading's mistakes, and their agreement then proves nothing. diff --git a/conformance/materialization/015_nested_sealed_template/expected.yaml b/conformance/materialization/015_nested_sealed_template/expected.yaml new file mode 100644 index 0000000..9b24137 --- /dev/null +++ b/conformance/materialization/015_nested_sealed_template/expected.yaml @@ -0,0 +1,21 @@ +--- +values: + a: + values: from-inner + origin: [{sealed: {template: $inner, path: $.leaf}}, schema] + shape: + values: + type: + values: scalar + origin: [schema] + scalar_type: + values: string + origin: [schema] + origin: [schema] +origin: [yaml] +shape: + values: + type: + values: object + origin: [schema] + origin: [schema] diff --git a/conformance/materialization/015_nested_sealed_template/input.yaml b/conformance/materialization/015_nested_sealed_template/input.yaml new file mode 100644 index 0000000..d9e1a25 --- /dev/null +++ b/conformance/materialization/015_nested_sealed_template/input.yaml @@ -0,0 +1,2 @@ +--- +{} diff --git a/conformance/materialization/015_nested_sealed_template/meta.yaml b/conformance/materialization/015_nested_sealed_template/meta.yaml new file mode 100644 index 0000000..456811e --- /dev/null +++ b/conformance/materialization/015_nested_sealed_template/meta.yaml @@ -0,0 +1,26 @@ +--- +id: materialization/015_nested_sealed_template +title: A sealed_from chain expands to the template it ultimately names +invariants: [INV-015, INV-005, INV-044] +description: >- + A template entry may itself be sealed from another template. One + implementation expanded the chain; the other resolved exactly one level, and + because a `sealed_from` node declares no shape of its own the node defaulted + to object and materialized as `{}`. Two different objects from one schema: + + go: values: from-inner + origin: [{sealed: {template: $inner, path: $.leaf}}, schema] + rust: values: {} + origin: [{sealed: {template: $outer, path: $.mid}}] + + The corpus had no nested template, so neither runner could see it. Vector 003 + and 004 cover a single level only. + + Note what the expansion also settles, which the specification does not state: + the origin names the INNERMOST template and path — the place the value came + from — not the mount point where the chain was entered. This vector pins that + answer; SPEC 5 should say it (audit semantic F-06). + + Expansion is bounded at 64 levels in both implementations. Without a bound a + chain of templates referring to each other is an unbounded loop over input a + caller supplies (audit semantic F-07). diff --git a/conformance/materialization/015_nested_sealed_template/schema.yaml b/conformance/materialization/015_nested_sealed_template/schema.yaml new file mode 100644 index 0000000..0b9ee36 --- /dev/null +++ b/conformance/materialization/015_nested_sealed_template/schema.yaml @@ -0,0 +1,20 @@ +--- +model: "0.2" +templates: + $inner: + $.leaf: + shape: scalar + scalar_type: string + default: from-inner + $outer: + $.mid: + sealed_from: + template: $inner + path: $.leaf +root: + shape: object + children: + a: + sealed_from: + template: $outer + path: $.mid diff --git a/docs/en/architecture.md b/docs/en/architecture.md index ed6acf0..d7b5608 100644 --- a/docs/en/architecture.md +++ b/docs/en/architecture.md @@ -8,7 +8,7 @@ model, and everything else here exists to keep that document honest. ```mermaid graph TD - S[SPEC.md — 46 numbered invariants] --> V[conformance/ — 37 vectors] + S[SPEC.md — 46 numbered invariants] --> V[conformance/ — 38 vectors] S --> M[docs/spec-vector-map.md] V --> G[go/ — reference implementation] V --> R[rust/ — reference implementation] diff --git a/docs/spec-vector-map.md b/docs/spec-vector-map.md index 46f3d5a..1ec6043 100644 --- a/docs/spec-vector-map.md +++ b/docs/spec-vector-map.md @@ -34,7 +34,7 @@ for months and "nothing else is" never was. | INV-012 | No `default` member on a canonical node | 4.5 | `validation/005_default_member` | | INV-013 | Origin grammar — exactly four forms | 5.2 | `materialization/001_origin_yaml`
`materialization/002_origin_schema`
`materialization/003_origin_sealed`
`materialization/004_origin_sealed_schema`
`materialization/009_normalize_map`
`materialization/010_normalize_empty_object`
`validation/001_origin_yaml_schema_conflict`
`validation/007_origin_duplicate_term`
`validation/008_origin_terms_out_of_order`
`validation/009_origin_sealed_schema_repeated` | | INV-014 | Origin is classification, not history | 5.2 | `invalid/005_origin_in_authoring_input` | -| INV-015 | `sealed` MUST carry `template` and `path` | 5.2 | `invalid/007_sealed_missing_path`
`materialization/003_origin_sealed`
`materialization/004_origin_sealed_schema` | +| INV-015 | `sealed` MUST carry `template` and `path` | 5.2 | `invalid/007_sealed_missing_path`
`materialization/003_origin_sealed`
`materialization/004_origin_sealed_schema`
`materialization/015_nested_sealed_template` | | INV-016 | `sealed` + `yaml` → invalid | 5.3 | `invalid/001_sealed_yaml_conflict`
`invalid/002_sealed_yaml_schema_conflict` | | INV-017 | `yaml` + `schema` → invalid | 5.3 | `validation/001_origin_yaml_schema_conflict` | | INV-018 | Empty origin → invalid | 5.3 | `validation/002_origin_empty` | diff --git a/go/conformance/conformance_test.go b/go/conformance/conformance_test.go index 9d9839d..667b9ba 100644 --- a/go/conformance/conformance_test.go +++ b/go/conformance/conformance_test.go @@ -92,7 +92,10 @@ var expectedCounts = map[string]int{ // 14 since materialization/014_hostile_keys: mapping keys were written // verbatim while the quoting rule applied only to values, so a child named // `a: b` made this package return bytes that are not YAML. - "materialization": 14, + // 15 since materialization/015_nested_sealed_template: a sealed_from chain + // expanded in one implementation and stopped after one level in the other, + // producing two different objects from one schema. + "materialization": 15, // 7 since model 0.2: `invalid/008_cyclic_primitive_declaration` was // removed. It asserted that a repeated primitive name is a cycle, which // INV-005 no longer treats as one — under INV-035 a primitive's payload diff --git a/project.yaml b/project.yaml index 3472eb7..f8811db 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: 'b419bbddff3053f3899c418015276de11f1f6874bb78fbe987426817bc235312' + buildHash: '830f62fda35e03d00836602fe9ab7d6237209f1c83277c5b79c10cf691605d4f' cicSign: 'TBD' cicSignedCA: certificate: "TBD — filled by the release process with the CIC Root CA certificate" diff --git a/rust/src/materialize.rs b/rust/src/materialize.rs index ba567d9..ab73d33 100644 --- a/rust/src/materialize.rs +++ b/rust/src/materialize.rs @@ -184,7 +184,28 @@ impl<'a> Ctx<'a> { Ok(node) } - /// Resolve a `sealed_from` to the template entry it names. + /// Resolve a `sealed_from` chain to the entry it ultimately names. + /// + /// A template entry may itself be sealed from another template, and this + /// used to resolve exactly ONE level: the entry was returned as the + /// effective schema, and since a `sealed_from` node declares no shape of + /// its own the node defaulted to `object` and materialized as `{}`. + /// + /// Measured against the Go implementation on a two-level chain: + /// + /// go: values: from-inner + /// origin: [{sealed: {template: $inner, path: $.leaf}}, schema] + /// rust: values: {} + /// origin: [{sealed: {template: $outer, path: $.mid}}] + /// + /// Two different objects from one schema, and the corpus had no nested + /// template, so neither runner could see it. Note what the expansion also + /// settles: the origin names the INNERMOST template and path, not the + /// mount point — the place the value actually came from. + /// + /// The depth bound matches Go's. It is not decoration: without it a chain + /// of templates referring to each other is an unbounded loop over input a + /// caller supplies. fn resolve_sealed<'s>( &self, sn: &'s SchemaNode, @@ -193,26 +214,42 @@ impl<'a> Ctx<'a> { where 'a: 's, { - let Some(ref r) = sn.sealed_from else { - return Ok((sn, None, None)); - }; - let Some(entry) = self.schema.template_entry(&r.template, &r.path) else { - return Err(Error::new( - code::TEMPLATE_NOT_FOUND, - "INV-015", - Stage::SchemaLoad, - path, - format!("no template `{}` declares `{}`", r.template, r.path), - )); - }; - Ok(( - entry, - Some(Sealed { + const MAX_TEMPLATE_DEPTH: usize = 64; + + let mut node = sn; + let mut sealed = None; + let mut content = None; + + for depth in 0..=MAX_TEMPLATE_DEPTH { + let Some(ref r) = node.sealed_from else { + return Ok((node, sealed, content)); + }; + if depth == MAX_TEMPLATE_DEPTH { + return Err(Error::new( + code::TEMPLATE_NOT_FOUND, + "INV-005", + Stage::SchemaLoad, + path, + "template expansion exceeded the maximum depth", + )); + } + let Some(entry) = self.schema.template_entry(&r.template, &r.path) else { + return Err(Error::new( + code::TEMPLATE_NOT_FOUND, + "INV-015", + Stage::SchemaLoad, + path, + format!("no template `{}` declares `{}`", r.template, r.path), + )); + }; + sealed = Some(Sealed { template: r.template.clone(), path: r.path.clone(), - }), - entry.content.as_ref(), - )) + }); + content = entry.content.as_ref(); + node = entry; + } + unreachable!("the loop returns or errors on every iteration") } /// A declared position takes a payload of its own arity (INV-008). diff --git a/rust/tests/conformance.rs b/rust/tests/conformance.rs index 3a42b7c..1388c87 100644 --- a/rust/tests/conformance.rs +++ b/rust/tests/conformance.rs @@ -29,7 +29,7 @@ const CORPUS: &str = "../conformance"; // things the serialization can do to an address before the model sees the // document. Both exist because the two implementations DISAGREED on real input // and the corpus could not see it. -const EXPECTED: [(&str, usize); 3] = [("materialization", 14), ("invalid", 12), ("validation", 11)]; +const EXPECTED: [(&str, usize); 3] = [("materialization", 15), ("invalid", 12), ("validation", 11)]; fn vectors(group: &str) -> Vec { let dir = Path::new(CORPUS).join(group);