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);