text-1a: the textlayout resolver at oracle v0 - #70
Conversation
The producer-side implementation of the single text-resolution contract
(docs/wg/feat-paragraph/text-layout.md) at its smallest honest profile:
one style run of horizontal LTR text, no wrapping, no fallback, no
synthesis — everything outside the profile is a typed refusal, and
coverage grows by oracle version as the RFD prescribes.
- hermetic environment: a manifest of caller-verified font bytes
(FontKey = host-checked content digest); empty by default, so
undeclared text refuses instead of reaching for a system font
- the immutable artifact: resolved face identity, positioned glyphs
with cluster mappings, fractional advances, line metrics, logical
and ink bounds — consumers project, they never re-resolve
- the y-flip owned once: outlines stream through the crate's own sink
in y-down local px, pinned by test against the measured Ahem box
- identity locked by architecture tests: dependency perimeter exactly
{rustybuzz}; no fontdb, no render contract, no backend, no clock,
no I/O in src
This is the Web family's producer, not an engine-wide text service —
the D-M shaped-text join (docs/wg/consolidation/n0-join-point.md)
stays open, and the dependency direction is websem -> textlayout only.
Unit tests assert the ground truth measured in the text-0 probe rounds
and crux spike. Lock delta is pure addition (rustybuzz already in the
graph via vendored usvg).
Merged as #68. The gate ladder, admitted numeric domain, bake posture, hermetic font environment, corpus-growth law, outlines-first lowering, and the resolver crate now bind the text arc's rungs. The D-M shaped-text stage stays open — text-2 is the spike that closes it.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThe PR adds the ChangesText layout oracle
Sequence Diagram(s)sequenceDiagram
participant Caller
participant resolve
participant Environment
participant rustybuzz
participant ResolvedTextLayout
Caller->>resolve: submit AttributedText and Environment
resolve->>Environment: find declared family
Environment-->>resolve: return FontResource
resolve->>rustybuzz: shape printable-ASCII text
rustybuzz-->>resolve: return glyph placements
resolve->>ResolvedTextLayout: store metrics, bounds, glyphs, and font bytes
ResolvedTextLayout-->>Caller: return immutable layout
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: 4
🧹 Nitpick comments (1)
crates/textlayout/src/resolve.rs (1)
224-230: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winConsider accumulating the pen in font units.
pen_xaccumulates already-scaledf32values. Each addition adds rounding error, so a long run drifts from the exact scaled sum of integer advances. Accumulating the advance in font units and scaling at use keeps every recordedxand the total advance exact to one rounding step.♻️ Proposed refactor
let mut glyphs = Vec::with_capacity(shaped.len()); - let mut pen_x = 0.0f32; + let mut pen_units: i64 = 0; @@ glyphs.push(PlacedGlyph { glyph_id, - x: pen_x, + x: pen_units as f32 * scale, advance: pos.x_advance as f32 * scale, cluster: info.cluster, }); - pen_x += pos.x_advance as f32 * scale; + pen_units += i64::from(pos.x_advance); } + let pen_x = pen_units as f32 * scale;🤖 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/textlayout/src/resolve.rs` around lines 224 - 230, Update the pen position handling around glyph placement so `pen_x` accumulates unscaled font-unit advances, and apply `scale` only when assigning each glyph’s `x` and computing the final total. Preserve integer advance accumulation and ensure every recorded position is converted to `f32` once after scaling.
🤖 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/resolve.rs`:
- Around line 163-173: The face validation around NON_OUTLINE_GLYPH_TABLES must
reject faces without an outline source before resolution proceeds. Add a
positive requirement for at least one of glyf, CFF , or CFF2, and extend the
unsupported-table checks to include monochrome bitmap tables EBDT and EBLC while
preserving the existing color/bitmap refusals.
- Line 186: Update the descent calculation to convert face.descender() to f32
before applying negation, avoiding i16::MIN overflow while preserving the
existing scaling behavior.
- Around line 177-181: Update the units_per_em handling in the face-resolution
flow to validate the original face.units_per_em() value, rejecting zero and any
value outside 16..=16384 with ResolveError::UnparseableFace before converting or
dividing. Preserve the existing family context and only compute scale after
validation succeeds.
In `@crates/textlayout/tests/architecture.rs`:
- Around line 55-81: Extend the FORBIDDEN denylist in the architecture test to
reject the compile-time ambient-input macros include_bytes!, include_str!, env!,
and option_env!. Keep the existing path-based checks and add entries that catch
these macro usages so undeclared files or environment values cannot bypass the
resolver restrictions.
---
Nitpick comments:
In `@crates/textlayout/src/resolve.rs`:
- Around line 224-230: Update the pen position handling around glyph placement
so `pen_x` accumulates unscaled font-unit advances, and apply `scale` only when
assigning each glyph’s `x` and computing the final total. Preserve integer
advance accumulation and ensure every recorded position is converted to `f32`
once after scaling.
🪄 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: 2e6316ab-9bc4-4ef0-a8b4-22dfdb7b3008
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (11)
AGENTS.mdCargo.tomlcrates/textlayout/Cargo.tomlcrates/textlayout/src/artifact.rscrates/textlayout/src/environment.rscrates/textlayout/src/lib.rscrates/textlayout/src/resolve.rscrates/textlayout/tests/architecture.rscrates/textlayout/tests/oracle_v0.rsdocs/wg/consolidation/index.mddocs/wg/consolidation/text-oracle.md
| for tag in NON_OUTLINE_GLYPH_TABLES { | ||
| if face | ||
| .raw_face() | ||
| .table(rustybuzz::ttf_parser::Tag::from_bytes(tag)) | ||
| .is_some() | ||
| { | ||
| return Err(ResolveError::UnsupportedFaceFormat { | ||
| family: text.style.family.clone(), | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check whether the oracle tests pin behavior for outline-less or bitmap-only faces.
set -euo pipefail
fd -t f -e rs . crates/textlayout --exec rg -n -C4 'UnsupportedFaceFormat|EBDT|EBLC|CBDT|glyf|CFF' {}Repository: gridaco/nothing
Length of output: 2401
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== file size =="
wc -l crates/textlayout/src/resolve.rs
echo "== resolve outline =="
ast-grep outline crates/textlayout/src/resolve.rs --view expanded || true
echo "== resolve relevant section =="
sed -n '1,260p' crates/textlayout/src/resolve.rs
echo "== search textlayout outline behavior =="
rg -n -C3 'glyph_bounding_box|outline|ResolvedTextLayout|ink_union|NON_OUTLINE_GLYPH_TABLES|resolve\(' crates/textlayout/src crates/textlayout/testsRepository: gridaco/nothing
Length of output: 27748
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== tests with UnsupportedFaceFormat =="
fd -t f -e rs . crates/textlayout --exec rg -n -C5 'UnsupportedFaceFormat|color or bitmap|outline glyph|outlineGlyph|outline' {} || true
echo "== oracle fixtures list =="
git ls-files crates/assets fixtures crates/textlayout | sed -n '1,200p'
echo "== inspect test files sizes =="
for f in crates/textlayout/tests/*.rs; do
echo "--- $f ($(wc -l < "$f") lines) ---"
sed -n '1,260p' "$f"
doneRepository: gridaco/nothing
Length of output: 28285
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== textlayout Cargo.toml =="
cat crates/textlayout/Cargo.toml
echo "== search web-first ahem =="
git ls-files | rg '(^fixtures/web-first/fonts/ahem\.ttf$|fixtures/web-first/)' | sed -n '1,200p'
echo "== find NotoColorEmoji =="
fd -a 'NotoColorEmoji-Regular\.ttf$|unifont.*\.otf$' . | sed -n '1,80p'
python3 - <<'PY'
from pathlib import Path
for p in Path('.').rglob('*.ttf'):
s = p.read_bytes()[:20]
if b'COLR' in s or b'CBDT' in s or b'CBLC' in s or b'EBDT' in s or b'EBLC' in s or b'glyf' in s or b'CFF ' in s or b'CFF2' in s or s.startswith((b'ttcf',)):
print(p, s.hex())
for p in Path('.').rglob('*.otf'):
s = p.read_bytes()[:20]
if b'COLR' in s or b'CBDT' in s or b'CBLC' in s or b'EBDT' in s or b'EBLC' in s or b'glyf' in s or b'CFF ' in s or b'CFF2' in s or s.startswith((b'ttcf',)):
print(p, s.hex())
PYRepository: gridaco/nothing
Length of output: 13793
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== textlayout Cargo.toml =="
cat crates/textlayout/Cargo.toml
echo "== repository tracked fonts likely bitmap/color =="
python3 - <<'PY'
from pathlib import Path
def scan(path):
try:
data = path.read_bytes()[:2000]
except Exception as e:
print(path, "ERR", e)
return
tags = []
for tag in [b'COLR', b'CBDT', b'CBLC', b'EBDT', b'EBLC', b'glyf', b' C ', b' C V ', b'CFF ', b'CFF2', b'sbgx', b'sbix']:
if tag in data:
tags.append(tag.decode('latin1', 'replace'))
if tags or data[:4] in [b'ttcf']:
print(path, data[:4].decode(errors='replace'), tags)
for ext in ('.ttf', '.otf', '.ttc'):
for p in Path('.').rglob('*' + ext):
if p.parts[:3] == ('fixtures', 'web-first', 'fonts'):
scan(p)
PY
echo "== fixture index mentions for textlayout fonts =="
rg -n 'Ahem|unifont|Noto Color|ColorEmoji|unifont|otf|ttf' fixtures/textlayout crates/textlayout/fixtures fixtures | sed -n '1,200p' || trueRepository: gridaco/nothing
Length of output: 12202
Refuse faces that have no outline source.
The current guard only rejects color/bitmap tables such as COLR, CBDT, CBLC, sbix, and SVG . It allows faces that parse but have no outline source (glyf, CFF , CFF2) and allows monochrome bitmap tables such as EBDT/EBLC. Resolution then succeeds with advance-only glyphs, and both ink_bounds() and outline() treat them as if the glyph has no pixel, like the space glyph. Add a positive outline-source requirement and include the applicable bitmap tables in the refusal list.
🤖 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/textlayout/src/resolve.rs` around lines 163 - 173, The face validation
around NON_OUTLINE_GLYPH_TABLES must reject faces without an outline source
before resolution proceeds. Add a positive requirement for at least one of glyf,
CFF , or CFF2, and extend the unsupported-table checks to include monochrome
bitmap tables EBDT and EBLC while preserving the existing color/bitmap refusals.
Source: Coding guidelines
| let units_per_em = | ||
| u16::try_from(face.units_per_em()).map_err(|_| ResolveError::UnparseableFace { | ||
| family: text.style.family.clone(), | ||
| })?; | ||
| let scale = size / f32::from(units_per_em); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
rustybuzz 0.20.1 Face units_per_em return type ttf-parser valid range
💡 Result:
In rustybuzz 0.20.1, the Face::units_per_em method typically returns a u16 value [1][2]. While some internal representations or wrapper methods may cast this to an i32 (for instance, to match HarfBuzz C API expectations) [3], the underlying data type provided by the ttf-parser dependency is a u16 [4][5]. Regarding the valid range, the units_per_em value is guaranteed to be within the range 16..=16384 [1][2][4][5]. This range is enforced by the ttf-parser library, which provides the font parsing logic for rustybuzz [4][6].
Citations:
- 1: https://docs.rs/rustybuzz/latest/rustybuzz/struct.Face.html
- 2: https://doc.servo.org/rustybuzz/struct.Face.html
- 3: https://doc.servo.org/src/rustybuzz/hb/face.rs.html
- 4: https://docs.rs/ttf-parser/latest/ttf_parser/head/struct.Table.html
- 5: https://traffloat.github.io/api/master/ttf_parser/struct.Face.html
- 6: https://github.com/harfbuzz/ttf-parser/blob/main/c-api/lib.rs
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate relevant files =="
fd -a 'resolve\.rs|Cargo\.toml|Cargo\.lock' . | sed 's#^\./##' | head -100
echo
echo "== dependency versions =="
rg -n 'rustybuzz|ttf-parser|ttfautohint' Cargo.toml Cargo.lock ./crates/textlayout -S 2>/dev/null | head -200 || true
echo
echo "== resolve.rs outline and relevant sections =="
if [ -f crates/textlayout/src/resolve.rs ]; then
wc -l crates/textlayout/src/resolve.rs
ast-grep outline crates/textlayout/src/resolve.rs 2>/dev/null || true
sed -n '140,195p' crates/textlayout/src/resolve.rs
fiRepository: gridaco/nothing
Length of output: 6111
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== cargo metadata for rustybuzz 0.20.1 =="
cargo metadata --format-version 1 2>/tmp/cargo-meta.stderr | jq -r '
.packages[] | select(.name == "rustybuzz" and .version == "0.20.1") | {
name, version, source, manifest_path, deps: (.dependencies // []) | map({name, version, optional})
}
' || {
cat /tmp/cargo-meta.stderr
exit 1
}
echo
echo "== check local rustybuzz API source if present =="
python3 - <<'PY'
from pathlib import Path
for p in Path('.rg','/cargo/registry/src').glob('**/rustybuzz-0.20.1/src/**/*.rs'):
if 'units_per_em' in p.read_text(errors='replace'):
text = p.read_text(errors='replace')
print(f'FILE {p}')
for i,line in enumerate(text.splitlines(),1):
if 'units_per_em' in line or 'pub fn units_per_em' in line:
print(f'{i}: {line}')
PY
echo
echo "== inspect rustybuzz Face units_per_em references in cargo source cache =="
python3 - <<'PY'
from pathlib import Path
roots = [Path.home()/'.cargo/registry/src', Path('/usr/local/cargo/registry/src')]
hits=[]
for root in roots:
if root.exists():
for p in root.rglob('*.rs'):
if 'units_per_em' in p.read_text(errors='ignore'):
for line in p.read_text(errors='ignore').splitlines():
if 'units_per_em' in line:
hits.append((p,line.strip()))
for p,line in hits[:50]:
print(f'{p}: {line}')
PY
echo
echo "== local API references =="
rg -n 'fn units_per_em|units_per_em\(\)' --glob '*.rs' . | head -120Repository: gridaco/nothing
Length of output: 145
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== check if Cargo.lock contains exact rustybuzz version and source =="
rg -n '^name = "rustybuzz"$|rustybuzz = "=0.20.1"|checksum ="|source = ' Cargo.lock -A8 -B2 | sed -n '1,220p'
echo
echo "== search local .cargo if available =="
if [ -d "$HOME/.cargo/registry/src" ]; then
rg -n 'pub fn units_per_em|fn units_per_em|units_per_em\(&self\)' "$HOME/.cargo/registry/src" -g '*rustybuzz*' 2>/dev/null | head -80 || true
else
echo "NO $HOME/.cargo/registry/src"
fi
echo
echo "== exact local units_per_em usages =="
rg -n 'units_per_em|face\.units_per_em|ttf_parser::.*units_per_em' --glob '*.rs' . | head -120Repository: gridaco/nothing
Length of output: 9535
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== inspect artifact.rs units_per_em path =="
sed -n '1,45p' crates/textlayout/src/artifact.rs
sed -n '185,210p' crates/textlayout/src/artifact.rs
sed -n '225,245p' crates/textlayout/src/artifact.rs
echo
echo "== inspect resolve error handling =="
sed -n '25,90p' crates/textlayout/src/resolve.rs
sed -n '166,184p' crates/textlayout/src/resolve.rs
echo
echo "== inspect usvg ttf-parser units_per_em assumptions =="
sed -n '1190,1250p' third_party/usvg/src/text/layout.rsRepository: gridaco/nothing
Length of output: 10104
🌐 Web query:
rustybuzz 0.20.1 Face units_per_em i32 source units_per_em
💡 Result:
In rustybuzz version 0.20.1, the units_per_em method on the Face (or hb_font_t) structure returns an i32 [1][2]. The underlying implementation stores the value as a u16 field within the struct [1][2]. The units_per_em method acts as a wrapper that casts this internal u16 value to an i32 [1][3]: #[inline] pub fn units_per_em(&self) -> i32 { self.units_per_em as i32 } This casting is provided to maintain API compatibility or convenience, while the actual font data (guaranteed to be in the range 16..=16384) is held in the u16 storage field [4][5][1].
Citations:
- 1: https://doc.servo.org/src/rustybuzz/hb/face.rs.html
- 2: https://doc.servo.org/rustybuzz/hb/face/struct.hb_font_t.html
- 3: https://gitdab.com/andodeki/makepad/src/branch/nigig-dev-reexports/libs/rustybuzz/src/hb/face.rs
- 4: https://docs.rs/rustybuzz/latest/rustybuzz/struct.Face.html
- 5: https://doc.servo.org/rustybuzz/struct.Face.html
Reject malformed units_per_em instead of treating parser output as dead.
units_per_em is read from face.units_per_em() then stored as u16, so an out-of-range report through that API path can still exist if the parser is not the bounded units_per_em(...) API. Reject the face when the value is outside 16..=16384 or is zero to prevent division by zero and non-finite geometry scaling.
🤖 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/textlayout/src/resolve.rs` around lines 177 - 181, Update the
units_per_em handling in the face-resolution flow to validate the original
face.units_per_em() value, rejecting zero and any value outside 16..=16384 with
ResolveError::UnparseableFace before converting or dividing. Preserve the
existing family context and only compute scale after validation succeeds.
| ascent: f32::from(face.ascender()) * scale, | ||
| // ttf-parser reports descent as a negative distance; the artifact | ||
| // states it as a positive reach below the baseline. | ||
| descent: f32::from(-face.descender()) * scale, |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Widen before negating the descender.
face.descender() returns i16. -i16::MIN overflows, which panics in a debug build and wraps in a release build. Widen to f32 first, then negate.
🐛 Proposed fix
- descent: f32::from(-face.descender()) * scale,
+ descent: -f32::from(face.descender()) * scale,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| descent: f32::from(-face.descender()) * scale, | |
| descent: -f32::from(face.descender()) * scale, |
🤖 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/textlayout/src/resolve.rs` at line 186, Update the descent calculation
to convert face.descender() to f32 before applying negation, avoiding i16::MIN
overflow while preserving the existing scaling behavior.
| const FORBIDDEN: &[&str] = &[ | ||
| // Ambient font discovery — the environment is a manifest of bytes. | ||
| "fontdb", | ||
| "core_text", | ||
| "CoreText", | ||
| "dwrite", | ||
| "fontconfig", | ||
| // Render contracts and backends stay out of the resolver. | ||
| "rframe", | ||
| "websem", | ||
| "n0_model", | ||
| "csscascade", | ||
| "stylo", | ||
| "skia", | ||
| // Ambient inputs: resolution is a pure function of declared inputs. | ||
| "std::fs", | ||
| "std::net", | ||
| "std::env", | ||
| "std::time", | ||
| "std::io", | ||
| "SystemTime", | ||
| "Instant", | ||
| // Brace and glob imports could smuggle any of the above past a | ||
| // path-shaped needle; single-path imports only. | ||
| "use std::{", | ||
| "use std::*", | ||
| ]; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Reject compile-time ambient inputs.
include_bytes!, include_str!, env!, and option_env! bypass this denylist. These macros can embed undeclared files or environment values while this test still passes.
Proposed fix
"Instant",
+ "include_bytes!",
+ "include_str!",
+ "env!",
+ "option_env!",As per coding guidelines, never allow an unsupported construct to produce a silent wrong pixel.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const FORBIDDEN: &[&str] = &[ | |
| // Ambient font discovery — the environment is a manifest of bytes. | |
| "fontdb", | |
| "core_text", | |
| "CoreText", | |
| "dwrite", | |
| "fontconfig", | |
| // Render contracts and backends stay out of the resolver. | |
| "rframe", | |
| "websem", | |
| "n0_model", | |
| "csscascade", | |
| "stylo", | |
| "skia", | |
| // Ambient inputs: resolution is a pure function of declared inputs. | |
| "std::fs", | |
| "std::net", | |
| "std::env", | |
| "std::time", | |
| "std::io", | |
| "SystemTime", | |
| "Instant", | |
| // Brace and glob imports could smuggle any of the above past a | |
| // path-shaped needle; single-path imports only. | |
| "use std::{", | |
| "use std::*", | |
| ]; | |
| const FORBIDDEN: &[&str] = &[ | |
| // Ambient font discovery — the environment is a manifest of bytes. | |
| "fontdb", | |
| "core_text", | |
| "CoreText", | |
| "dwrite", | |
| "fontconfig", | |
| // Render contracts and backends stay out of the resolver. | |
| "rframe", | |
| "websem", | |
| "n0_model", | |
| "csscascade", | |
| "stylo", | |
| "skia", | |
| // Ambient inputs: resolution is a pure function of declared inputs. | |
| "std::fs", | |
| "std::net", | |
| "std::env", | |
| "std::time", | |
| "std::io", | |
| "SystemTime", | |
| "Instant", | |
| "include_bytes!", | |
| "include_str!", | |
| "env!", | |
| "option_env!", | |
| // Brace and glob imports could smuggle any of the above past a | |
| // path-shaped needle; single-path imports only. | |
| "use std::{", | |
| "use std::*", | |
| ]; |
🤖 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/textlayout/tests/architecture.rs` around lines 55 - 81, Extend the
FORBIDDEN denylist in the architecture test to reject the compile-time
ambient-input macros include_bytes!, include_str!, env!, and option_env!. Keep
the existing path-based checks and add entries that catch these macro usages so
undeclared files or environment values cannot bypass the resolver restrictions.
Source: Coding guidelines
The first code rung of the text arc (gridaco/nothing#69), executing against the method ratified in #68. It builds the second real text producer — the thing D-M's shaped-text stage has been gated on since it was recorded.
What this is
crates/textlayout— the Web family's text resolution oracle, implementing the text-layout RFD at its smallest honest profile:Why an artifact, not a shaping helper
The obvious build — shape, flatten to outlines, done — lands correct pixels and unlocks nothing. D-M asks whether a neutral shaped-text representation and font-key boundary can be produced by both engines; outlines are geometry that deliberately carries no font identity at all. A producer that discards its shaped-text artifact is not a second producer in the D-M sense, and the discovery would come at text-2 with nothing to push against n0's artifact.
So the artifact exists and is inspectable inside the producer, even though it never crosses
rframe. Outline lowering is its first consumer.Equally deliberate: this is the Web family's producer, not an engine-wide text service. Building it as the anointed shared resolver would decide D-M high by construction and make text-2 theater. The dependency direction is
websem -> textlayoutonly; join-low stays a live outcome.The profile, and what it refuses
v0 resolves one style run of printable-ASCII, horizontal, LTR text — no wrapping, fallback, or synthesis. The repertoire is an explicit admit-list enforced by the resolver, so out-of-profile input refuses by byte position regardless of what any font happens to cover. Refusals: unknown family, unparseable face, color/bitmap face, out-of-profile character, missing glyph, out-of-profile shaping (offsets, vertical advance, negative advance), invalid size.
Identity locked by architecture tests: perimeter exactly
{rustybuzz}across every dependency-table shape plus no build script; no ambient font database, render contract, backend, clock, or I/O insrc.Verified adversarially
Three reviewers with distinct lenses (RFD conformance, correctness, repo laws) were prompted to refute the crate, and did — against fonts outside the fixture corpus. Every must-fix is folded in:
is_control()guard (Cf, not Cc) and entered the artifact as a phantom space glyph; U+2028 evaded the line-terminator claim; ZWSP/soft-hyphen were silently substituted by the shaper's hide-default-ignorables pass. The admit-list closed all six at once.y_advanceand negative advances were dropped, flattening GPOS vertical pen movement onto the baseline.outline()accepted any caller-built glyph, so a glyph id from another font could be streamed through this artifact's face. Now index-based over recorded placements.=0.20.1soORACLE_VERSION's stability claim is mechanical; both architecture locks hardened;FontKey's doc no longer claims a verification it does not perform.Declined with reasons recorded: in-crate hashing (a sha2 perimeter edge is an owner decision; the brief assigns verification to the host, gated when
n0_cli --fontlands), aMissingGlyphtest (unreachable through Ahem over ASCII — an honest comment beats a fake pin), line-gap inLineMetrics(declared deferral).Ground truth
Tests assert the numbers measured in the text-0 probe rounds and the crux spike — glyph 58, 1000-unit advances, the 0.8/0.2 em split, the y-flip vertices — not values derived from this crate. Lock delta is pure addition.
Also promotes the text-oracle brief to ratified now that #68 has merged.
Next in the arc: the websem
<text>arm consuming this crate, then the text cells andsvg-textgraduation.