Structural references through native Talos composition¶
| Date | 24 September 2026 |
| Work item | Experiment E2 - test structural-reference composition |
| Design reference | §6.9 Secret references and validation stages, §18.1 Phase 0 |
| Artifacts | experiments/e2-structural-references/, evidence under experiments/e2-structural-references/evidence/ |
| Decision enabled | Whether a reference syntax and resolution order exist that keep native Talos composition, and what each candidate would demand of a renderer. This is the input to the compiler specification and renderer selection. No syntax is selected here. |
1. Question¶
Design §6.9 lists three reference candidates (an explicit YAML tag, an opted-in marked string and an external path binding) and states a preference: "resolving only effective references after upstream composition if upstream typed machinery permits it". It also requires E2 to "test early materialization with a recorded provenance superset, non-string targets, overridden references and embedded content", to "distinguish effective artifact dependencies from dependencies needed to reproduce the complete source graph", and excludes a custom merge engine: "if no candidate works within that constraint, revise the reference design before implementation."
The question is therefore which candidate and order give the same materialized configuration as
native Talos composition of the same fragments written with literal values, on every case the
issue names, with composition and validation left to the pinned upstream talosctl.
| Acceptance criterion | Answered in |
|---|---|
| 1. Every candidate with non-string typed targets, overrides, aliases and embedded documents | §3.1 (the cases), §4 (the matrix) |
| 2. Effective-only dependencies against early-resolution supersets; artifact against source-reproduction dependencies | §6.2 |
| 3. Native merge parity and validation of the materialized output, with collisions and rejections | §4, §6.3 |
| 4. Candidate failures and renderer implications, no unsupported semantics selected | §6.4 |
2. Candidates and orders¶
Every case is written once, with each reference as a canonical !ref <name> tag. The prototype
derives four forms of the same intent from it:
| Form | A reference to reg-pass in a fragment |
|---|---|
| reference-free (native) | password: e2-registry-password |
| tag | password: !bwref reg-pass |
| marked | password: "bwref:reg-pass", a reference only in a fragment the case opts in |
| binding | the key is absent (a list element whose only key is bound stays as {}); bindings.tsv holds fragment, reference, document and path |
The binding form deletes each bound key and prunes a mapping that only bound keys filled, but not a
list element: an element whose only key is bound stays as an empty {}. The bytes case's CA
element has no other field, so its binding fragment holds acceptedCAs with the one element
- {} (evidence/gen/bytes.txt). Resolving a binding adds a missing key at the end of its mapping.
Each candidate is resolved in two orders:
- Late: compose the candidate's fragments with
talosctl machineconfig patch, then resolve references in the composed configuration. After composition no fragment's opt-in is visible, so everybwref:string counts, and every binding is applied in fragment order. - Early: resolve each fragment on its own, then compose the resolved fragments. This resolves every reference in every fragment: the provenance superset.
3. What was built¶
bwref(Go,gopkg.in/yaml.v3): generates the four forms, resolves one candidate in one order and prints where it resolved, and finds sentinel values after composition. It never merges. Only an embedded document the case declares as identified is parsed.run/all: per base, case, candidate and order, composes, resolves, re-reads the result through a no-op patch (machine: {}) so that native and candidate outputs come from the same encoder, compares the output byte for byte with native, and runstalosctl validate --strict.run/collect-evidence: copies the committable text intoevidence/and refuses it if it holds a leak-refusal pattern (§3.4).
Outcomes: parity (byte-identical to native), differs, rejected (a talosctl decode failure,
at stage compose or normalize), refused (the resolver refused). The validation verdict is a
separate column. Every expectation was written into case.yaml before the first run; that is
asserted, not shown by the history, because the cases and the prototype landed in one commit.
3.1 Cases¶
| Case | Target | Kind |
|---|---|---|
| string | registry auth.password |
string |
| int | machine.features.kubePrism.port |
integer |
| bool | machine.install.wipe |
boolean |
| bytes | machine.acceptedCAs[0].crt |
base64 bytes, inside a list element |
| map | registry auth |
a whole mapping (a typed struct) |
| override-literal | a reference in fragment 1, a literal at the same path in fragment 2 | override |
| override-ref | a literal in fragment 1, a reference at the same path in fragment 2 | override |
| alias | &secret on one registry password, *secret on another |
alias |
| embedded-yaml | cluster.inlineManifests[name=e2-secret].contents, identified YAML |
embedded |
| embedded-json | machine.files[path=/var/etc/e2/config.json].content, identified JSON |
embedded |
| unidentified-embedded | the same manifest, not identified | embedded, opaque |
| document | certificates of a separate TrustedRootsConfig document |
multi-document |
| collision | fragment 1 (not opted in) holds "!bwref reg-user" and "bwref:reg-pass" as literals; fragment 2 holds a real reference |
collision |
| type-mismatch | a string value in the integer port field | rejection |
| invalid-value | cluster.network.dnsDomain = e2 not a domain |
rejection by validation |
The last two are rejection cases: native itself fails, so the expected candidate result is the same
failure. unidentified-embedded is a negative control for §6.9's "opaque to all three forms unless
explicitly identified": no candidate can express a reference inside it, so no candidate is expected
to reach parity there.
3.2 Bases¶
The issue binds the experiment to the fixture's real machine configurations. Every case runs on two bases:
- fixture: the control-plane configuration
fixtures/bin/upread back from the running node, validated incontainermode (the fixture runs Talos in Docker); - generated:
talosctl gen configoutput for a control plane with freshgen secrets, validated inmetalmode.
The two bases must agree cell by cell on everything but the message, whose line numbers differ.
3.3 Pinned versions¶
| Component | Version |
|---|---|
talosctl |
v1.13.6, SHA-256 540c5e7cb0d3fa3a9b2e1c717ced212727b73bcaf0cf9cf9ba2472ec381041d4, from fixtures/versions.env as of c74f953 |
| Go | 1.27.1 |
gopkg.in/yaml.v3 |
v3.0.1 |
evidence/run.txt records the prototype commit of the capture (dc26305), zero uncommitted
inputs, and the talosctl version and digest. Later commits change only comments in run/all and
run/lib.sh, and collect-evidence: it stops on a failed scan instead of reading it as clean, and
packs each case into one file (§7.7). The evidence was collected again from the same capture with
that collector; unpacked, it is byte-identical to the first collection.
3.4 Synthetic values and the leak scan¶
Every reference value is a synthetic e2-... string, integer or boolean in case.yaml. The bases
carry real-format but throwaway cluster secrets: the fixture's bundle and a fresh gen secrets
bundle. Neither bundle and no materialized output is committed; evidence/ holds the diffs of each
output against native and against its base, each base with every secret line redacted, and the
SHA-256 of every output.
The leak-refusal patterns are every scalar of 16 characters or more in both secrets bundles, plus the fixture's own scan list: 35 patterns. Two controls stand behind a clean scan:
- Key material covered: every base64 value of 40 or more characters in each base is covered by a pattern; the same check with a pattern file that covers nothing finds 11 such lines per base.
- Scan fires:
collect-evidenceplants one pattern in a control file and requires the scan to report exactly that file before the real scan runs.
Result: 35 patterns, control fired, 0 hits in 54 files (evidence/leak-scan.tsv). The per-cell
files are packed one text file per case (§7.7).
3.5 Reproduction¶
fixtures/bin/up
E2_OUT=<an empty directory outside the checkout> experiments/e2-structural-references/run/all
E2_OUT=<the same directory> experiments/e2-structural-references/run/collect-evidence
fixtures/bin/down
Three captures gave the same matrix: the last before the prototype was committed, one at
a7f4bb5, and the collected one at dc26305, which differs from a7f4bb5 only in writing - for
an empty cell (§7.6). Between the last two, the fixture base's 88 output digests were identical;
the generated base's 88 differed in every output, as they must, because each run generates a fresh
secrets bundle. Only the collected capture is retained; the comparisons with the earlier two are
recorded here, not in evidence/.
4. The matrix¶
15 cases × 7 rows (native plus 3 candidates × 2 orders) × 2 bases = 210 rows, 0 differing from the
expectation, both bases agree (evidence/summary.txt). Per base:
| Case | tag late | tag early | marked late | marked early | binding late | binding early |
|---|---|---|---|---|---|---|
| string | differs | parity | parity | parity | parity | parity |
| int | rejected | parity | rejected | parity | parity | parity |
| bool | rejected | parity | rejected | parity | parity | parity |
| bytes | rejected | parity | rejected | parity | parity | parity |
| map | rejected | parity | rejected | parity | parity | parity |
| override-literal | parity | parity | parity | parity | differs | parity |
| override-ref | differs | parity | parity | parity | parity | parity |
| alias | differs | parity | parity | parity | parity | parity |
| embedded-yaml | parity | parity | parity | parity | parity | parity |
| embedded-json | parity | parity | parity | parity | parity | parity |
| unidentified-embedded | differs | differs | differs | differs | refused | refused |
| document | differs | parity | parity | parity | parity | parity |
| collision | differs | parity | differs | parity | parity | parity |
| type-mismatch (native rejected) | rejected | rejected | rejected | rejected | rejected | rejected |
| invalid-value (native invalid) | differs | parity | parity | parity | parity | parity |
Totals per base: early reaches parity for every candidate on all 13 cases other than type-mismatch and unidentified-embedded (invalid-value included: there parity means failing validation as native does), matches the native rejection on type-mismatch, and fails as designed on unidentified-embedded. Late reaches parity on 3 cases with the tag, 8 with the marked string and 12 with the binding.
Three late parities are coincidental and do not count in a candidate's favour:
- tag late, override-literal: the tag is stripped (§5 below), but fragment 2's literal overwrites the stripped value anyway.
- binding late, override-ref: the binding happens to sit in the last fragment, so applying it after composition gives the value composition would have kept.
- binding late, bytes: the binding's path is
machine/acceptedCAs[0]/crt, an index into the fragment's list applied to the composed list. Neither base hasacceptedCAs, so index 0 is the fragment's element: the{}placeholder (§2) that the binding fills. On a base that already held a CA, the binding would overwrite the base's first CA, and if composition appends the fragment's element to the base's list, the placeholder would stay behind as an empty{}CA. Neither was run.
5. What each late failure is¶
These are observed behaviours of talosctl v1.13.6 strategic merge, recorded with the exact output.
- A tag on a string field is dropped silently. Composition keeps the scalar and loses the tag,
so the reference name becomes the value (
evidence/cases/generated/string.txt, sectiontag-late/diff-native):
The same happens through an alias, in the separate TrustedRootsConfig document
(certificates: roots) and on the real reference of the collision case. On invalid-value the
output becomes dnsDomain: dns-domain, which is a valid DNS name, so validate --strict passes
a configuration holding the wrong value. Validation is no guard against this failure.
- A tag or a marker on a non-string field is rejected by typed decoding before any resolver sees
it, as §6.9 anticipated ("upstream typed decoding may reject it before patching"):
cannot construct !bwref `prism-port` into int
cannot construct !bwref `wipe` into bool
cannot construct !bwref `reg-auth` into v1alpha1.RegistryAuthConfig
cannot construct !!str `bwref:p...` into int
cannot construct !!str `bwref:wipe` into bool
cannot construct !!str `bwref:r...` into v1alpha1.RegistryAuthConfig
illegal base64 data at input byte 5
(each prefixed error decoding document /v1alpha1/ (line 1): error decoding to *v1alpha1.Config:
yaml: construct errors: line <n>:). A marker on a string field survives composition as a string.
The rejection is shown for the integer, boolean and struct targets. On the bytes target both
forms fail with the same message, talosctl base64-decoding the placeholder text, but for
different reasons. The marker's byte 5 is the : of bwref:extra-ca, and : is never valid
base64, so every marked value is rejected on a bytes field whatever its reference name. The tag's
text is the bare name extra-ca, whose byte 5 is a -: only the tag's rejection depends on the
name. A tag on a string field loses its tag and keeps its text (above), so a tag whose name
happens to be valid base64 would likely compose into wrong bytes silently. That was not run.
A marker cannot read as a YAML boolean, because of its bwref: prefix. A tag could, but only
through YAML 1.1 compatibility: yaml.v3 decodes the strings yes, on, off and their
variants into a boolean field whatever their tag. Whether talosctl's decoder (its errors say
construct, where yaml.v3's say unmarshal) does the same was not checked, and no such name
was run.
- A marker cannot honour a per-fragment opt-in after composition. In the collision case,
fragment 1 is not opted in and holds the literal bwref:reg-pass; resolved late, it is taken for
a reference:
The tag has no such collision: a quoted "!bwref reg-user" is a plain string to YAML and stayed
literal in every order.
- A binding applied after composition cannot see fragment order. In override-literal, fragment 2
overrides the bound path with a literal; late, the binding from fragment 1 is applied on top of
it:
Getting this right late means knowing, per path, which fragment wrote last: modelling the merge, which §6.9 excludes.
6. What this decides¶
6.1 Criterion 1: every candidate on the required cases¶
All three candidates were run in both orders on integer, boolean, base64-bytes and whole-mapping targets, on both override directions, through an alias, in identified embedded YAML and JSON, in a second configuration document, and on collision and rejection cases, on two real bases (§3.1, §4).
6.2 Criterion 2: effective and superset dependencies¶
For each cell the matrix records the superset (every reference the source fragments name) and the effective set (the references whose values are in the materialized output):
- Late, the effective set is what the resolver resolved. It is correct only where the cell
reaches parity; in override-literal the binding reports
reg-passas effective, and it is not. - Early, resolution necessarily covers the superset, so the effective set had to be measured
separately: a second pass resolves every reference to a distinguishable sentinel (strings
E2SENTINELplus six digits, integers from 61000), composes, and finds which sentinels survive. This is a measurement method, not a proposed remedy (§6.9: "Dummy substitution is an investigation option").
Observed:
- override-literal, early: superset
reg-pass, effective set empty. The artifact depends on no secret, yet rendering it from source needsreg-pass. - alias: one reference, effective at two paths (both registry passwords).
- bool: a boolean sentinel cannot be told from a literal, so the early effective set is
unidentifiable by this method (reported as
+unidentifiable: wipe). Only the binding's late resolution names it. How to derive an effective boolean dependency early is open.
In the matrix's effective column, - means "measured, none survived" in a cell that composed
(override-literal early) and "not measured" in a rejected or refused cell; the observed column
tells them apart.
The distinction the design asks for follows directly:
- Artifact dependencies are the effective set: what the published artifact contains, and what retention, rotation and lost-dependency classification must track for it.
- Complete source-reproduction dependencies are the superset: under early resolution every reference in every fragment must be readable to render again from source, including overridden ones. A lost overridden secret leaves the artifact intact but blocks re-rendering from source.
6.3 Criterion 3: parity, validation, collisions and rejections¶
- Parity is byte-for-byte against the native composition of the same fragments with literal
values. Each base is itself re-read through the no-op patch before use, so every output is
talosctl's own encoding. The re-read leavestalosctl's output byte-identical (control on the generated base); it reformats the fixture's read-back configuration once (control on the fixture base), before any case runs. A late cell's output, which the resolver wrote, goes through the same re-read, so the re-read adds no difference of its own. - Validation ran on every materialized output. All 124 parity cells validated as native did, with the same message, including invalid-value, where native and every parity cell fail with:
- Rejections: type-mismatch is rejected natively with
cannot construct !!str `e2-not-...` into int. Every early cell and the binding late are rejected with the same message; the binding late at the re-read rather than at composition, and so at a different line of the document (line 23 on the fixture base and 24 on the generated one, against line 4 for native and every early cell), because the key it fills is absent from its fragments. The tag and marked late are rejected on their own placeholder instead (§5).run/allcompares a rejection with its expectation by outcome only, and the two bases with each other on everything but the message: that the messages are the same was read from themessagecolumn ofmatrix.tsv, not checked by the harness. The resolver refusal, binding only:
bwref: binding v1alpha1 cluster/inlineManifests[name=e2-secret]/contents|yaml/stringData/password:
cluster/inlineManifests[name=e2-secret]/contents is not an identified embedded document
- Collisions: §5 above. Only the marked string late misread a literal.
6.4 Criterion 4: candidate failures and renderer implications¶
Late resolution fails for every candidate on at least one required case, so §6.9's preferred order ("if upstream typed machinery permits it") is not available within the no-merge-engine constraint:
| Candidate | Late failure | Why it is not fixable in the renderer without a merge engine |
|---|---|---|
| tag | dropped silently on strings; rejected on integer, boolean and struct fields | composition discards it before the renderer runs |
| marked | rejected on integer, boolean, bytes and struct fields; misreads literals | typed decode runs first; opt-in is a fragment property composition erases |
| binding | wrong value when a later fragment overrides; a list index names the fragment's list, not the composed one | needs per-path last-writer knowledge |
Early resolution reaches native parity for all three candidates on every case run but the two
where parity is not expected (§4), with one scope limit for the binding in embedded YAML, below.
It hands talosctl only reference-free fragments, so upstream composition and validation are
untouched. What each candidate then demands of a renderer:
- tag: a YAML parser that keeps tags, since resolution must happen before any typed decode.
Quoted text that looks like a tag is safely literal. An unresolved tag inside unidentified
embedded text is left in the output as
!bwref app-passand validation passes. - marked: a per-fragment opt-in declaration that the renderer honours before composition. Its collision defence exists only early. Like the tag, an unresolved marker inside unidentified text is left in the output and validation passes.
- binding: bindings keyed by fragment, document and path, kept in step as fragments are edited
(list selectors such as
[name=e2-secret]included). It is the only candidate that refuses a reference into unidentified embedded content instead of leaving reference text behind. (Resolved late, a fragment that holds only bound values is empty, andtalosctlrefuses an empty patch, so such a fragment must be left out of composition: §7.1. Early, each fragment is refilled first.) A list element whose only key is bound is left as a{}placeholder (§2), so the fragment's list keeps its length. Early, the placeholder is filled before composition and never reachestalosctl. Late, it is composed first; §4 shows the one base-dependent consequence. - binding, embedded YAML: the refill adds a missing key at the end of its mapping. For a typed
field that should not matter, because
talosctldecodes into its typed configuration and writes the fields in that type's order, though no case here binds a typed key that is not last in its fragment either. An embedded document is an opaque string totalosctl, so there key order is part of the bytes. The embedded-yaml case bindsstringData.password; the binding fragment has nostringDataat all (evidence/gen/embedded-yaml.txt, pruned as in §7.2), the refill appends it aftermetadata, and that matches native only becausestringDatais the last key. A binding to a key that is not last, such asmetadata.name, would move that key to the end of its mapping and so differ from native. That was not run.
For identified embedded documents, every candidate re-serializes the whole document it resolves
into: the prototype writes JSON with sorted keys and no whitespace, and YAML through yaml.v3 with
two-space indentation. The native literal form was derived through the same encoders, so neither
embedded-json's nor embedded-yaml's parity shows that an author's hand-formatted literal would
survive: JSON would differ in formatting, and for YAML, keeping an author's indentation, comments
or quoting is unproven. The tag form of the embedded JSON ("token": !bwref app-token) is not
JSON either; the prototype parses it as YAML.
For every candidate, early resolution means the compiler identity must read the whole superset (§6.2), and the release must record the effective set separately if dependency tracking is to be exact.
7. Failures hit while building it¶
7.1 An emptied binding fragment is not a patch¶
The first capture had 5 unexpected rows, all binding late, all rejected with
config not found: a fragment whose only content was bound became {}, which talosctl refuses
as a patch. This was a runner defect, not a candidate result. run/all now leaves an empty fragment
out of composition and records that it did (*.skipped).
7.2 An emptied mapping re-encoded in flow style¶
Deleting every bound key under stringData left stringData: {}, which re-encoded in flow style
and, once refilled by the binding, broke early parity. The prototype now prunes mappings that only
bound keys filled, and clears flow style on a mapping it adds a key to.
7.3 The superset column read the wrong field¶
The first capture listed fragment names, not references, in the superset column. Fixed before any result was read from it.
7.4 Leak patterns too short, then too wide¶
A 24-character floor missed the 23-character bootstrap token; the floor is now 16. Taking every
long scalar of each base as a pattern then redacted image references and URLs as well; patterns
now come from the secrets bundles and the fixture's list, and the key-material control (§3.4) shows
no key-like base value is left uncovered. That control first passed silently when its own awk
failed; it now requires the no-pattern run to find lines.
7.5 fixtures/bin/up has no help option¶
fixtures/bin/up --help ignores its argument and starts the fixture. The pipe it was run through
killed it part-way, leaving a partial fixture; fixtures/bin/down removed it cleanly, and a normal
up followed. No evidence came from the partial fixture.
7.6 The evidence failed the whole-tree whitespace check¶
An empty message cell ended every such matrix row in a tab, the talosctl version line ended in a
space, and talosctl validate ends its message with a blank line. The first two are the runner's
own formats and were fixed; the validation messages are kept verbatim, inside the per-case packs
(§7.7), each of which ends with a closing marker line.
7.7 One evidence file per cell made the pull request unreviewable¶
The first collection wrote about 750 files, one per cell output. GitHub serves no pull-request diff
of more than 300 files, and the advisory review skipped with PR diff too large for GitHub API (HTTP
406), skipping. while its check still reported success. collect-evidence now packs each case into
one text file, each cell file after a ==> <cell>/<file> <== line: 55 files. Unpacking the packs
reproduces every earlier per-cell file byte for byte.
8. Limits¶
- One Talos version. Every verdict rests on
talosctlv1.13.6. Other contracts are not present and no candidate result may be generalised to them. - Strategic merge only. JSON6902 is refused for the v1.13 multi-document configuration (fixtures report §5); no JSON patch was tried.
- Fifteen cases, one machine type. Both bases are control-plane configurations. Not covered:
worker configurations, duration and IP/CIDR fields, a list as a reference target, list-element
overrides (the bytes case's list merged into a base with no such list, §4),
$patch: delete, more than two fragments, a tag whose reference name is valid base64 or a YAML 1.1 boolean word (§5), a binding to an embedded-YAML key that is not the last in its mapping, and a hand-formatted embedded literal (§6.4). - Embedded-YAML parity is shown for a last key only. The binding refills a key at the end of its mapping, and the one embedded-YAML reference is to the last key (§6.4). Parity there, late or early, is not shown for a binding to any other key.
- The fixture base validates in container mode. Its read-back configuration is a Docker node's;
metalvalidation applies only to the generated base. - Boolean effective dependencies are unmeasured early (§6.2).
- The sentinel pass has two flaws no case reaches. It reports every value that is neither a
string nor an integer (a float, a null, binary) as "a boolean value cannot be told apart from a
literal"; the only such value here is bool's
wipe: true. Integer sentinels are 61000 plus ten times the reference's index plus the leaf's, so in a value with eleven or more leaves an integer leaf could take the next reference's sentinel, and an integer literal equal to a sentinel would be counted as a surviving reference. No value here has more than two leaves (map'sreg-auth), one reference is an integer (int'sprism-port), and no case fragment or base underevidence/base/holds a 61xxx number. - Harness properties no capture reached. Each fails closed or did not arise:
- The
E2_OUTguard compares the path as given, not canonicalized, so a path reaching the checkout through..or a symlink would pass it. run/collect-evidencerequires the leak-scan control to be the scan's only hit (run/collect-evidence:89). A real leak alongside it would stop the collection as "control did not fire", not name the leaking file.- Its
needpreflight (run/collect-evidence:14) listscp, which it does not use, and omitstail,awk,sort,wcandmktemp, which it does. - The resolver is disposable. Its choices (per-fragment opt-in, path syntax, alias handling by replacing the anchor node) are this experiment's, not a proposed design.
- Error messages carry value prefixes.
talosctl's decode errors quote the first characters of the offending value (e2-not-...,bwref:p...). A secret placed in a wrongly typed field would leak its first seven characters into a validation error. This belongs to sensitivity and provenance tracking.
9. Alternatives and decision enabled¶
Compared: tag, marked string and binding, each resolved late and early; the reference-free literal form is the baseline. Not compared: a custom merge engine (excluded by §6.9), general interpolation (excluded), substring references in opaque text (§6.9 references whole content initially).
Decision enabled: §6.9's preferred late order is ruled out on this evidence, and early materialization with a recorded superset is the order that kept native composition for all three syntaxes on every case where parity was expected. That holds within the cases' scope (§8), most visibly for the binding in embedded YAML, where parity is shown only for a last key (§6.4). The syntax choice is not decided by composition parity, because all three reach it early on these cases; it turns on the renderer implications in §6.4, which the compiler specification must weigh. Adopting early resolution is a change to §6.9's stated preference and belongs in the design once accepted.
10. Hand-off¶
To sensitivity and provenance tracking:
- Every resolution is recorded as reference, document and path (
resolutions.tsv), and each early cell has the sentinel paths that survived composition (sentinel-found.tsv). An alias puts one reference at two paths. talosctldecode errors quote a value prefix (§8).- An unresolved tag or marker inside unidentified embedded text passes validation (§6.4).
To the compiler specification: §6.4 and §9. Open there: the effective-dependency method for booleans, whether a renderer must refuse leftover reference text, and how bindings follow fragment edits.