text-2: the D-M deciding spike — shaped text joins low - #73
Conversation
…fusals Push one real text run from each producer — textlayout at oracle v0, n0's Skia Paragraph oracle — at a spike-local candidate neutral shaped-text + font-key contract, and record where the boundary holds and where it fails. The spike is the two-producer experiment the join-point finding gated the text stage on; like the vector spike, it lives in n0's unit-test build and survives as the witness behind the taken decision. Measured (Darwin-arm64; a new platform declares through one loud CI round-trip, the corpus's ramp-quantization protocol): - Shaping facts join bit-exactly: glyph ids, pen positions, advance. The content key matches by construction only — n0's artifact carries process-local identity and cannot state it. - Metric facts arrive scaler-quantized (2^-14 per metric from the CoreText build) against the Web side's exact font-unit arithmetic. - Two outline extractions (Skia get_path; ttf-parser stream) agree byte-exactly at every probed anchor and are bilevel on the lattice; n0's glyph replay through its oracle's live Font paints a 432-byte non-bilevel fringe on the lattice itself, and a policy-stripped control (alias, unhinted, no subpixel) matches the outlines byte-exactly — the fringe is the anti-alias mask policy. - Realizing the candidate requires a digest→bytes environment parameter and gains an undeclared-key refusal — the resource boundary rframe's standing identity refuses and n0::glyphless is named for. - Beyond the overlap the second producer still does not exist: line structure refuses by type; styled runs and variable instances are not expressible in v0's input surface. The decided refusals gain their locks: rframe's architecture test now holds "no shaped-text fact" under D-M-text provenance and "no resource reference" under the contract's standing identity (phase 3 revisits that one on its own image evidence); textlayout enters n0 as a dev-dependency for this evidence alone, held dev-only by a section-membership lock in the spike; the websem text compiler's outline lowering is re-documented as the low join's mandate rather than a posture keeping the decision open.
…below the join The delegated decision, recorded: shaped text joins low, on the two-producer spike (crates/n0/src/text_join_spike.rs). D-M is complete — vector high (2026-07-23), text low (2026-08-05). The join-point finding gains "The text-stage evidence": the measured arms, the decision, and the re-opening bar — a declared resource environment entering the contract for another fact kind on its own evidence AND a measured need for cross-contract glyph replay; producer maturity alone does not re-open, because the deciding legs (metric fact identity, pixel-visible replay policy, the one-meaning tripwire) are independent of how much text the Web producer can state. Propagated to every record that said the stage was open: the charter (D-M registry row; phase-2 mechanism and exit — the engine-of-record route gates byte-exact under the text-oracle method, n0's private text stays gated by its own duty-cycle oracles), the consolidation index, web-first (dated subsequent-status note), the adoption patrol, the text-oracle brief, the paint-vocabulary gap report (text leaves go engine-private), topology (dated amendment reconciling the shaped-text bridge as a contract each engine's own producer implements), and the CLAUDE.md textlayout row.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThe PR closes the D-M shaped-text decision at a low join. Web and n0 retain engine-local text artifacts, font identity, and replay policy. Tests verify the boundary, while documentation records the decision and supporting evidence. ChangesShaped-text join
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (5)
crates/n0/src/text_join_spike.rs (4)
590-596: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winMake the fringe assertion prove the claim it states.
The comment says every differing byte is a partial-coverage value the outline render does not contain. The two assertions compare only counts. Two disjoint byte sets of size 432 would also satisfy them. Check the containment directly so the evidence matches the prose.
♻️ Set-based containment check
Add a helper next to
differing_bytes:/// Indices where `a` and `b` differ. fn differing_indices(a: &[u8], b: &[u8]) -> Vec<usize> { assert_eq!(a.len(), b.len()); a.iter() .zip(b) .enumerate() .filter(|(_, (left, right))| left != right) .map(|(index, _)| index) .collect() }Then assert containment at the lattice anchor:
let replayed = realize_glyph_replay(&run, &font, LATTICE_ANCHOR); let resolved = realize_candidate_outlines(&run, &environment, LATTICE_ANCHOR).unwrap(); - assert_eq!(differing_bytes(&replayed, &resolved), on_lattice); - // The divergence is a smoothing fringe, not displaced ink: every - // differing byte is a partial-coverage value the outline render does - // not contain. - assert_eq!(non_bilevel_bytes(&replayed), on_lattice); + let differing = differing_indices(&replayed, &resolved); + assert_eq!(differing.len(), on_lattice); + // The divergence is a smoothing fringe, not displaced ink: every + // differing byte is a partial-coverage value the outline render does + // not contain. + assert_eq!(non_bilevel_bytes(&replayed), on_lattice); + for index in &differing { + let byte = replayed[*index]; + assert!( + byte != 0 && byte != 255, + "byte {index} differs at bilevel value {byte}: displaced ink, not a fringe" + ); + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/n0/src/text_join_spike.rs` around lines 590 - 596, Update the fringe validation near differing_bytes so it proves index containment rather than only matching counts. Add the differing_indices helper beside differing_bytes, then assert that every differing replayed index at LATTICE_ANCHOR has a non-bilevel replayed value and that the corresponding resolved outline value is bilevel/absent, preserving the existing count checks only if still useful.
708-727: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winClose the dotted-key bypass in the dependency lock.
The match tests
textlayout =andpackage = "textlayout". Cargo also accepts dotted keys, for exampletextlayout.workspace = trueortextlayout.path = "../textlayout". Neither form matches. If a future change declarestextlayout.workspace = trueunder[dependencies]and keeps the current[dev-dependencies]entry,declared_in_devstays true and the lock passes while n0 ships the Web producer. Match the dotted form too.Also capture the section name without a trailing comment. A line such as
[dev-dependencies] # spike onlycurrently compares unequal and fails with a confusing message.♻️ Widen the match and normalize the section name
for line in manifest.lines() { let trimmed = line.trim_start(); if trimmed.starts_with('[') { - section = trimmed.to_string(); + section = trimmed + .split('#') + .next() + .unwrap_or(trimmed) + .trim_end() + .to_string(); continue; } - let names_the_crate = - trimmed.starts_with("textlayout =") || trimmed.contains("package = \"textlayout\""); + let names_the_crate = trimmed.starts_with("textlayout =") + || trimmed.starts_with("textlayout.") + || trimmed.contains("package = \"textlayout\""); if names_the_crate {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/n0/src/text_join_spike.rs` around lines 708 - 727, Update the manifest scan in the dependency-checking loop to recognize both direct and dotted `textlayout` keys, including forms such as `textlayout.workspace` and `textlayout.path`, while preserving the existing dev-dependency validation. Normalize section headers by removing any trailing comment and surrounding whitespace before storing them in `section`, so headers like `[dev-dependencies] # spike only` compare correctly.
267-309: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDocument the hardcoded
face_indexthe same way the digest is documented.The doc comment explains why the digest is joined back from the host declaration. It does not explain
face_index: 0at line 307.candidate_from_webreadslayout.face().face_indexfrom its artifact, so the two projections differ in origin. If n0's artifact also cannot state a face index, record that as a third projection fact. If it can state one, read it instead of pinning the literal.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/n0/src/text_join_spike.rs` around lines 267 - 309, Document the origin of CandidateFontKey.face_index in candidate_from_n0 alongside the existing digest projection fact. Determine whether the n0 layout exposes an artifact face index: if so, populate face_index from that value; otherwise retain 0 and explicitly document why the n0 projection cannot provide it, contrasting it with candidate_from_web.
169-179: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider comparing float facts by bits to match the stated claim.
The module doc and the arm name state "bit-exactly".
assert_eq!onf32uses value equality, so-0.0and0.0compare equal. The measured values here are0.0,20.0,40.0,60.0, so the current result is correct. If you want the assertion to enforce the exact claim, project the floats throughto_bits().♻️ Bit-exact projection
- fn shaping_facts(&self) -> (&CandidateFontKey, f32, Vec<(u16, f32)>, f32) { + fn shaping_facts(&self) -> (&CandidateFontKey, u32, Vec<(u16, u32)>, u32) { ( &self.key, - self.font_size, + self.font_size.to_bits(), self.glyphs .iter() - .map(|glyph| (glyph.id, glyph.x)) + .map(|glyph| (glyph.id, glyph.x.to_bits())) .collect(), - self.advance, + self.advance.to_bits(), ) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/n0/src/text_join_spike.rs` around lines 169 - 179, Update shaping_facts to project every f32 value through to_bits(), including font_size, glyph x values, and advance, so assert_eq! enforces bit-exact comparisons and distinguishes values such as -0.0 and 0.0.docs/wg/consolidation/topology.md (1)
26-36: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueBroaden the redirect to reach the Text section.
The redirect at lines 30-35 is scoped to places where the doc names the shaped-text artifact "as a bridge contract". The Text section at lines 149-163 does not use that phrase. It states "The chassis owns the seam" at line 151 and "All of it migrates through the artifact, not around it" at line 157. Both read as a single shared artifact, which the amendment reverses. A reader who starts at the Text section gets the pre-amendment meaning.
Either widen the redirect to cover every mention of the shaped-layout artifact in this doc, or add a one-line pointer to this amendment inside the Text section.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/wg/consolidation/topology.md` around lines 26 - 36, Broaden the amendment in the document so it also redirects the Text section’s statements that imply a single shared shaped-text artifact, including “The chassis owns the seam” and “All of it migrates through the artifact.” Either make the existing redirect cover every such mention or add a concise pointer to this amendment within the Text section, preserving the per-engine text-resolution contract meaning.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/textlayout/src/lib.rs`:
- Around line 21-27: Update the module documentation link near the D-M
shaped-text stage reference to use an explicit same-repository relative Markdown
destination for docs/wg/consolidation/n0-join-point.md, avoiding intra-doc link
resolution and preserving the visible document path.
In `@crates/websem/src/svg.rs`:
- Around line 2358-2363: Rewrite the incomplete sentence in the `<text>`
contract documentation near the D-M shaped-text join discussion so it is
grammatically complete, while preserving the existing meaning about the
undecided posture, the mandated low join, and the contract carrying no text fact
or resource reference.
In `@docs/wg/consolidation/n0-join-point.md`:
- Line 259: Update the condition wording in the decision text to replace
“requires both of:” with “requires both:” while preserving the two following
conditions.
In `@docs/wg/consolidation/paint-vocabulary-gap.md`:
- Line 178: Update the “Text paint partition” entry to make clear that its Open
status covers only per-engine text-paint gates and is outside the taken vector
scope, consistent with the completed D-M text stage described in the referenced
consolidation documents.
In `@docs/wg/consolidation/web-first.md`:
- Around line 145-150: Update the shared-boundary list near the bullets for
shaped-text artifacts and resource references so it no longer contradicts the
subsequent status block and the established no-text-fact position. Remove or
clearly annotate those stale bullets as superseded, while preserving the
document’s conclusion that the shared contract gains neither a text fact nor a
resource reference.
---
Nitpick comments:
In `@crates/n0/src/text_join_spike.rs`:
- Around line 590-596: Update the fringe validation near differing_bytes so it
proves index containment rather than only matching counts. Add the
differing_indices helper beside differing_bytes, then assert that every
differing replayed index at LATTICE_ANCHOR has a non-bilevel replayed value and
that the corresponding resolved outline value is bilevel/absent, preserving the
existing count checks only if still useful.
- Around line 708-727: Update the manifest scan in the dependency-checking loop
to recognize both direct and dotted `textlayout` keys, including forms such as
`textlayout.workspace` and `textlayout.path`, while preserving the existing
dev-dependency validation. Normalize section headers by removing any trailing
comment and surrounding whitespace before storing them in `section`, so headers
like `[dev-dependencies] # spike only` compare correctly.
- Around line 267-309: Document the origin of CandidateFontKey.face_index in
candidate_from_n0 alongside the existing digest projection fact. Determine
whether the n0 layout exposes an artifact face index: if so, populate face_index
from that value; otherwise retain 0 and explicitly document why the n0
projection cannot provide it, contrasting it with candidate_from_web.
- Around line 169-179: Update shaping_facts to project every f32 value through
to_bits(), including font_size, glyph x values, and advance, so assert_eq!
enforces bit-exact comparisons and distinguishes values such as -0.0 and 0.0.
In `@docs/wg/consolidation/topology.md`:
- Around line 26-36: Broaden the amendment in the document so it also redirects
the Text section’s statements that imply a single shared shaped-text artifact,
including “The chassis owns the seam” and “All of it migrates through the
artifact.” Either make the existing redirect cover every such mention or add a
concise pointer to this amendment within the Text section, preserving the
per-engine text-resolution contract meaning.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 4a18ab35-450c-4760-8158-7b5be4d15897
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (16)
AGENTS.mdcrates/n0/Cargo.tomlcrates/n0/src/drawlist.rscrates/n0/src/text_join_spike.rscrates/rframe/src/frame.rscrates/rframe/tests/architecture.rscrates/textlayout/src/lib.rscrates/websem/src/svg.rsdocs/wg/consolidation/charter.mddocs/wg/consolidation/index.mddocs/wg/consolidation/n0-join-point.mddocs/wg/consolidation/paint-vocabulary-gap.mddocs/wg/consolidation/text-oracle.mddocs/wg/consolidation/topology.mddocs/wg/consolidation/web-first.mddocs/wg/consolidation/web-renderer-adoption.md
|
|
||
| The decision fixes where the *contract* boundary sits; it does not forbid the | ||
| engines from later sharing a shaping oracle as a utility below it. Re-opening | ||
| is a new registered decision and requires both of: a declared resource |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Correct the condition wording.
requires both of: is not valid technical prose. Use requires both: before the two conditions.
🧰 Tools
🪛 LanguageTool
[style] ~259-~259: Consider replacing this word to strengthen your wording.
Context: ...Re-opening is a new registered decision and requires both of: a declared resource e...
(AND_THAT)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/wg/consolidation/n0-join-point.md` at line 259, Update the condition
wording in the decision text to replace “requires both of:” with “requires
both:” while preserving the two following conditions.
Source: Linters/SAST tools
| | Other blends, tile/alignment, gradients, and image paints | `cg` is the vector seat, but these leaves are not adopted by the solid-fill proving scope. | Preserve the full field matrices and settle transform/math/serialization policy; amendment-dependent gradient behavior and resource-bearing image facts retain their own gates. | | ||
| | Stroke application | `cg` is the selected seat, but its current ungrouped surface is not conforming and remains unmapped into production. | Add grouped, repeatable applications without flattening, then run the stroke laws directly. | | ||
| | Text paint partition | Open and outside the taken vector scope. | Resolve the text-stage decision, tri-state run-fill amendment, decoration color, and full run stroke applications. | | ||
| | Text paint partition | Open and outside the taken vector scope. The text-stage decision has since resolved **low** (2026-08-05, [the text-stage evidence](./n0-join-point.md#the-text-stage-evidence)): text paints never cross the shared contract, so this partition is each engine's private question, not a shared-seat adoption. | Tri-state run-fill amendment, decoration color, and full run stroke applications — now gated per engine, below the join. | |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Clarify the status of the text paint partition.
If Open refers only to the remaining per-engine text-paint gates, make that scope explicit. docs/wg/consolidation/index.md Lines 69-74 and docs/wg/consolidation/n0-join-point.md Lines 245-281 mark the D-M text stage as complete. Use wording such as Per-engine and outside the taken vector scope.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/wg/consolidation/paint-vocabulary-gap.md` at line 178, Update the “Text
paint partition” entry to make clear that its Open status covers only per-engine
text-paint gates and is outside the taken vector scope, consistent with the
completed D-M text stage described in the referenced consolidation documents.
| > **Subsequent status (2026-08-05):** the D-M shaped-text stage is taken | ||
| > **low** on the two-producer spike | ||
| > ([the text-stage evidence](./n0-join-point.md#the-text-stage-evidence)): | ||
| > shaped text stays below the join in each engine's private tier, and the | ||
| > shared contract gains no text fact and no resource reference. The generic | ||
| > frontend trait remains deferred. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Reconcile the new status block with the shared-boundary list.
Line 149 states the shared contract gains no text fact and no resource reference. The shared-boundary list at lines 90-91 still declares that the common product carries "shaped-text artifacts" and "resource references and exact environment revisions". The two statements are opposed, and nothing marks lines 90-91 as superseded.
AGENTS.md line 112 and the new rframe architecture tests both hold the no-text-fact position, so lines 90-91 are the stale side. Strike or annotate those two bullets so the document states one answer.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/wg/consolidation/web-first.md` around lines 145 - 150, Update the
shared-boundary list near the bullets for shaped-text artifacts and resource
references so it no longer contradicts the subsequent status block and the
established no-text-fact position. Remove or clearly annotate those stale
bullets as superseded, while preserving the document’s conclusion that the
shared contract gains neither a text fact nor a resource reference.
The first Linux CI round reported, per the declaration protocol, and its measurements sharpen the record in two ways: - The FreeType build's metrics are exact (quantum 0) while its pen placement is not: each Ahem em advance arrives one 2^-16 step short — the mirror image of CoreText (exact pen x, 2^-14 metrics). Which of n0's resolution facts arrive exact is itself platform-dependent, so the shaping arm restructures: the platform-invariant join is glyph *identity*; placement is declared per platform like the metrics. - Skia's get_path is policy-tinted by default on FreeType (hinting moved the extracted outline by 60 lattice bytes). The candidate consumer now strips hinting explicitly — a consumer realizing meaning must — and the Linux replay divergence deliberately stays undeclared so the next round re-measures it against the corrected baseline. The paper's evidence bullets and the charter's D-M row are restated to match: glyph identities join bit-exactly; placement and metric facts are the scaler's, per platform.
The second Linux round refuted the hinting attribution: the 60-byte lattice divergence between backend outline extraction and the artifact's stream persists unhinted. The finding is sharper than the fix it disproves — on the FreeType build the scaler's quantum reaches pixels through *extraction itself* (one box edge lands off the lattice and fringes), so the artifact's own outline stream is the one platform-invariant realization, and everything through a backend font object is that backend's policy. The two pixel arms restructure accordingly, applying the corpus's sweep-before-failing lesson: one platform-invariant arm (the artifact stream is bilevel exact ink on the lattice, everywhere) and one declared realization matrix (extraction vs stream, quantum invariance, replay vs consumer, fringe shape, the policy-stripped control) measured in a single sweep — an undeclared platform now reports its complete matrix in one CI round. Darwin-arm64 is declared; Linux declares from the next round's report. The paper's outline bullet and the consumer's comment are restated to the measured mechanism.
…ding The swept round reported Linux's complete matrix, and it corrects the previous commit's mechanism story: extraction is byte-exact against the artifact stream on both declared builds — the 60 lattice bytes were never extraction. They are the producer's own scaler-quantized placement reaching pixels when realized exactly (one alpha code value along the run's final edge), while FreeType's glyph replay is lattice-exact and bilevel precisely because its subpixel phase snapping rewrites that quantized placement back onto the lattice. The two declared builds therefore fail in opposite directions: CoreText replay fringes where exact realization is clean; FreeType realization shows the producer's quantum where replay silently repairs it. A producer's facts and its replay policy cancel only inside the matched engine-private pair — split across a neutral contract, each is a silent wrong pixel somewhere. Declared in the matrix; the paper's placement and realization bullets restated to the measured mechanism.
text-2 — the D-M deciding spike: shaped text joins low
The join-point finding gated D-M's last open stage on one experiment: when the Web family gains a real shaped-text producer, push a text run from both it and n0 at a candidate neutral shaped-text + font-key contract, and observe whether the neutral boundary holds (text joins high —
rframegains a text fact) or breaks (text joins low — each engine keeps its own text artifact). Withtextlayoutlive (#70) and rendering (#71, #72), the two-producers-first gate was satisfied for the first time. This PR runs the spike, takes the decision (delegated by the owner for this stage), and wires it fully.The spike
crates/n0/src/text_join_spike.rs— in-tree and unit-test-only, the same witness shape as the vector stage'sdrawlist_vector_join_spike.rs.textlayoutentersn0as a dev-dependency for this evidence alone; a section-membership lock in the spike holds it out of the shipping graph. The candidate contract is spike-local by design: content-digest font key (digest + face index + variations), font size, baseline-relative glyph placements, advance, metrics — no live object, no registry address, no raster-facing flags.What it measured — on two declared platforms
The per-platform measurements follow the corpus's ramp-quantization protocol (an undeclared platform fails loudly with its full measured matrix; Linux mapped itself through the CI rounds on this branch):
The Web producer (
textlayout) states exact font-unit arithmetic on every platform. n0's producer facts pass through its scaler backend, and which facts arrive exact is itself platform-dependent — one producer's facts are not even self-consistent across platforms. And the two builds fail in opposite directions: CoreText replay fringes where exact realization is clean; FreeType realization shows the producer's quantum where its replay silently repairs it. A producer's facts and its replay policy cancel only inside the matched engine-private pair — split across a neutral contract, each is a silent wrong pixel somewhere.Plus the structural legs: realizing the candidate requires a digest→bytes environment parameter and gains an undeclared-key refusal — the exact boundary
rframe's standing identity refuses andn0::glyphlessis named for (n0's artifact carries process-local identity only; the digest never flows through its oracle). And beyond the overlap the second producer still does not exist: line structure refuses by type; styled runs and variable instances are not expressible in v0's input surface.The decision
Shaped text joins low (D-M text stage, 2026-08-05 — D-M is now complete). Each engine's private compiler and executor retain their own text artifact, font registry, glyph replay policy, and text item. Sharing stops at backend glyph/raster utilities, licensed by the glyph-identity agreement. Content-digest font identity stays a host-seam convention — the CLI
--fontsurface,textlayout::FontKey, and the bake manifests already share it by convergence — never a contract fact. The Web route keeps lowering glyphs to the artifact's own outline stream beforerframe: the one realization measured platform-invariant and byte-exact where the oracle gates.Re-opening is a new registered decision and requires both: a declared resource environment entering the shared contract for some other fact kind on its own evidence, and a measured need for cross-contract glyph replay. Producer maturity alone does not re-open it — the deciding legs are independent of how much text the Web producer can state.
Wired
docs/wg/consolidation/n0-join-point.md— status complete; the deciding-fact row resolved; new "The text-stage evidence" sectiondocs/wg/consolidation/charter.md— D-M registry row: both stages taken; phase-2 mechanism and exit updated to the per-engine routeindex.md,web-first.md(dated subsequent-status),web-renderer-adoption.md,text-oracle.md,paint-vocabulary-gap.md(text leaves go engine-private),topology.md(dated amendment),CLAUDE.md— every "stage remains open" statement resolvedcrates/rframe/tests/architecture.rs— two decided-refusal locks with separate provenance: no shaped-text fact (D-M text) and no resource reference (standing identity; phase-3 images revisit on their own evidence)crates/rframe/src/frame.rs,crates/textlayout/src/lib.rs,crates/websem/src/svg.rs— module docs state the decided boundaryAn adversarial four-lens review (laws, evidence validity, refute-the-verdict, record consistency) ran against the tree before the first commit; the refute lens graded the verdict sound, and its findings — stale open-stage statements, lock provenance, evidence-claim precision — are incorporated.