Skip to content
Merged
8 changes: 8 additions & 0 deletions .github/workflows/consolidation-gates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,14 @@ jobs:
--test svg_architecture
cargo test --locked -p n0_cli --test architecture

- name: Run the n0-path architecture locks
env:
FORCE_SKIA_BINARIES_DOWNLOAD: "1"
run: |
cargo test --locked -p websem --test architecture
cargo test --locked -p rframe --test architecture
cargo test --locked -p n0 --test architecture

legacy_pixel_sweep:
name: legacy pixel sweep
needs: scope
Expand Down
9 changes: 6 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,12 @@ What is true right now, so a session does not infer it from ambition.
artifact in the tree is the frozen v1 wasm package.
- **There is no n0 WebAssembly target.** `grida-canvas-wasm` binds
`crates/grida` only. The v2 port is priced work, not an assumption.
- **Taffy is the layout engine.** A house-built layout engine is a stated goal,
not a current fact. The wall that would force it is browser-grade intrinsic
sizing across a namespace-aware tree — not flex.
- **Taffy is the layout engine — where layout exists.** It sits in `htmlcss`,
`grida`, and `n0-model` only; the Web-first render path
(`websem → rframe → n0`) runs no layout of any kind today, and `n0-model`'s
resolve tier is never called on it. A house-built layout engine is a stated
goal, not a current fact. The wall that would force it is browser-grade
intrinsic sizing across a namespace-aware tree — not flex.
- **No conformance score may be produced or inspected.** The FLIP rule is
unratified ([gridaco/nothing#49](https://github.com/gridaco/nothing/issues/49)).
A corpus may be described; results may not be scored, aggregated, or presented
Expand Down
8 changes: 7 additions & 1 deletion crates/n0_cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,13 @@ cargo run -p n0_cli --bin n0 -- \
default admission is **best-effort**: the admitted subset renders and
every beyond-slice construct is declared on stderr with its node path and
reason (`degraded: skipped svg/polygon[1]: unsupported element <polygon>`);
a beyond-inventory dynamic surface samples as the Base view. Declared
a beyond-inventory dynamic surface that leaves the Base view honest (an
event handler, a CSS animation carrier) samples as the Base view, while a
beyond-inventory _animation element_ — active at document load in
Chromium, so its target's authored state never honestly renders — skips
its target in every view, declared at the target's path (one that cannot
be attributed to a skippable element, an `href` retarget or a
root-`<svg>` target, refuses in both admissions like `<script>`). Declared
holes, never guessed pixels — the patrol is per attribute and per
cascaded property, so an admitted element carrying a rendering attribute
or stylesheet value the slice does not consume becomes a declared hole,
Expand Down
9 changes: 6 additions & 3 deletions crates/websem/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,12 @@ what it refuses; this table is the compiler's map, not a second copy of it.
**Strict** refuses on the first construct outside the slice — the harness that
names the edge. **Best-effort**, the product default, compiles what it admits
and declares everything else as a named degradation: a subtree construct is
skipped by name at a stable path, a blocked dynamic surface resolves every
sample to Base. Where nothing degrades the two are frame-identical, and a law
checks that over the whole corpus.
skipped by name at a stable path, a blocked dynamic surface that leaves Base
honest resolves every sample to Base, and a beyond-inventory animation element
— active at document load, so its target's authored state never honestly
renders — skips its target in every view, declared at the target's path.
Where nothing degrades the two are frame-identical, and a law checks that
over the whole corpus.
Comment on lines +47 to +52

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Restates n0_cli/README.md's degradation semantics instead of linking to it.

This paragraph now nearly duplicates crates/n0_cli/README.md's wording on beyond-inventory animation-element skip/declare behavior (target's-authored-state-never-honestly-renders / skips-its-target-in-every-view / declared-at-the-target's-path). This file's own earlier text (unchanged, lines 39-40) already commits to crates/n0_cli/README.md being "the single statement of record for that slice and what it refuses" and this table being "the compiler's map, not a second copy of it" — the new detail goes past a map into a restatement that now needs to be kept in sync across two files.

✏️ Proposed trim back to a pointer
 **Strict** refuses on the first construct outside the slice — the harness that
 names the edge. **Best-effort**, the product default, compiles what it admits
 and declares everything else as a named degradation: a subtree construct is
-skipped by name at a stable path, a blocked dynamic surface that leaves Base
-honest resolves every sample to Base, and a beyond-inventory animation element
-— active at document load, so its target's authored state never honestly
-renders — skips its target in every view, declared at the target's path.
+skipped by name at a stable path; see `crates/n0_cli/README.md` for the exact
+degradation shapes (dynamic-surface `SamplesAsBase`, beyond-inventory
+animation-element target skips, and the unattributable-retarget refusal).
 Where nothing degrades the two are frame-identical, and a law checks that
 over the whole corpus.

Based on coding guidelines: "The admitted n0 slice must be documented only in crates/n0_cli/README.md; do not restate it elsewhere, and link to that statement of record."

📝 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.

Suggested change
skipped by name at a stable path, a blocked dynamic surface that leaves Base
honest resolves every sample to Base, and a beyond-inventory animation element
— active at document load, so its target's authored state never honestly
renders — skips its target in every view, declared at the target's path.
Where nothing degrades the two are frame-identical, and a law checks that
over the whole corpus.
skipped by name at a stable path; see `crates/n0_cli/README.md` for the exact
degradation shapes (dynamic-surface `SamplesAsBase`, beyond-inventory
animation-element target skips, and the unattributable-retarget refusal).
Where nothing degrades the two are frame-identical, and a law checks that
over the whole corpus.
🤖 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/websem/README.md` around lines 47 - 52, Trim the added
degradation-semantics paragraph in the websem README so it no longer restates
the beyond-inventory animation-element behavior described in
crates/n0_cli/README.md. Replace those details with a concise pointer to that
README as the single statement of record, while preserving only the compiler-map
context and frame-identical law as appropriate.

Source: Coding guidelines


Neither mode ever guesses a pixel. That is the invariant the patrols exist for:
a construct the compiler cannot honour must refuse loudly or be declared, and a
Expand Down
125 changes: 115 additions & 10 deletions crates/websem/src/svg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ use rframe::{
use std::sync::Arc;

use crate::effective_values::EffectiveValues;
use crate::svg_animation::{AnimationInventory, is_animation_element};
use crate::svg_animation::{AnimationError, AnimationInventory, is_animation_element};

/// Which grammar entry retained the source.
///
Expand Down Expand Up @@ -379,6 +379,14 @@ pub enum CompileError {
/// silently. (Scripts elsewhere on the page stay under the pinned
/// first-SVG-only entry contract and the closed sampling inventory.)
ScriptInCompiledSvg,
/// An animation element outside the closed sampling inventory. SMIL's
/// default `begin` is offset `0s`, so such an element is active the
/// moment Chromium loads the document: rendering its target's authored
/// state would be a wrong pixel, not a sampling gap. Strict refuses at
/// construction; best-effort skips the target and declares it. One that
/// cannot be attributed to a skippable element — an `href` retarget, a
/// root-`<svg>` target — refuses in both admissions, like `<script>`.
UnsupportedAnimation(AnimationError),
}

/// One retained, styled Web SVG source.
Expand All @@ -399,6 +407,11 @@ pub struct SvgFrameSource {
/// entry only. Fixed at construction so every sample recompile resolves
/// root sizing identically to Base.
initial_viewport: Option<InitialViewport>,
/// Targets of load-active authored-state overrides, by node, with the
/// declared reason. Best-effort only, fixed at construction: Base and
/// every sample recompile leave these elements out identically, so a
/// skip is a property of the retained source, never of one view.
override_skips: HashMap<NodeId, String>,
}

impl std::fmt::Debug for SvgFrameSource {
Expand Down Expand Up @@ -505,7 +518,7 @@ impl SvgFrameSource {
CascadeDriver::new(&mut session).style_document();

let mut degradations = Vec::new();
let (svg_root, compilation, animation) = {
let (svg_root, compilation, animation, override_skips) = {
let document = session.document();
let root = document.root_element().ok_or(CompileError::NoSvgRoot)?;
// xml5ever suspends its tokenizer at any <script> and the
Expand Down Expand Up @@ -552,15 +565,74 @@ impl SvgFrameSource {
if entry == SourceEntry::InlineHtml && subtree_contains_script(svg) {
return Err(CompileError::ScriptInCompiledSvg);
}
let mut walk_degradations = Vec::new();
let compilation = compile_svg_element(
svg,
&EffectiveValues::base(),
mode,
&mut degradations,
&mut walk_degradations,
initial_viewport,
&HashMap::new(),
)?;
let animation = AnimationInventory::inspect(svg, &compilation.top_level_shapes, entry);
(svg.node_id(), compilation, animation)
// A beyond-inventory animation element is active at document
// load (SMIL defaults `begin` to offset 0s): Chromium paints
// the overridden value, so the target's authored state cannot
// render as the Base view. Strict refuses on the first, like
// any beyond-slice construct. One that cannot be attributed to
// a skippable element — an `href` retarget, a root-`<svg>`
// target — is document-level and refuses in both admissions,
// exactly as `<script>` does. Best-effort recompiles with the
// targets left out, so each becomes a declared hole in every
// view rather than a wrong pixel in any.
let (compilation, walk_degradations, override_skips) =
if let Some(first) = animation.overrides().first() {
if mode == CompileMode::Strict {
return Err(CompileError::UnsupportedAnimation(first.error().clone()));
}
if let Some(document_level) = animation
.overrides()
.iter()
.find(|the_override| the_override.document_level())
{
return Err(CompileError::UnsupportedAnimation(
document_level.error().clone(),
));
}
let override_skips: HashMap<NodeId, String> = animation
.overrides()
.iter()
.map(|the_override| {
(
the_override.target(),
format!(
"its authored state is overridden at document load by \
the unsupported animation at {}: {}",
the_override.error().path(),
the_override.error().reason()
),
)
})
.collect();
let mut declared = Vec::new();
let compilation = compile_svg_element(
svg,
&EffectiveValues::base(),
mode,
&mut declared,
initial_viewport,
&override_skips,
)
.expect(
"narrowing the walk with declared skips cannot change \
document-level compilability",
);
(compilation, declared, override_skips)
} else {
(compilation, walk_degradations, HashMap::new())
};
degradations.extend(walk_degradations);
(svg.node_id(), compilation, animation, override_skips)
};
if mode == CompileMode::BestEffort {
for blocker in animation.blockers() {
Expand All @@ -581,6 +653,7 @@ impl SvgFrameSource {
mode,
degradations,
initial_viewport,
override_skips,
})
}

Expand Down Expand Up @@ -651,13 +724,15 @@ impl SvgFrameSource {
);
// The degradation set is a property of the retained source, declared
// once at construction; the sample recompile reproduces the same
// skips deterministically and its sink is discarded.
// skips — the walk's and the authored-state overrides' alike —
// deterministically, and its sink is discarded.
let compilation = compile_svg_element(
svg,
&values,
self.mode,
&mut Vec::new(),
self.initial_viewport,
&self.override_skips,
)
.expect("time changes effective values, not compilability of the retained source");
Ok(compilation.frame)
Expand Down Expand Up @@ -737,6 +812,11 @@ impl std::fmt::Display for CompileError {
"<script> inside the compiled inline SVG can rewrite the authored state \
the Base view renders, so it is refused in both admissions"
),
CompileError::UnsupportedAnimation(error) => write!(
f,
"{error}; it is active at document load, so the authored state it \
overrides cannot render as the Base view"
),
}
}
}
Expand Down Expand Up @@ -1297,6 +1377,7 @@ fn compile_svg_element(
mode: CompileMode,
degradations: &mut Vec<Degradation>,
initial_viewport: Option<InitialViewport>,
override_skips: &HashMap<NodeId, String>,
) -> Result<FrameCompilation, CompileError> {
// The outer <svg> is the canvas contract: a rendering attribute or a
// cascaded value the slice cannot honor here would wrong every pixel,
Expand Down Expand Up @@ -1363,6 +1444,7 @@ fn compile_svg_element(
values,
mode,
degradations,
override_skips,
nodes: Vec::new(),
top_level_shapes: Vec::new(),
next_id: 0,
Expand Down Expand Up @@ -1410,6 +1492,11 @@ struct ChildWalk<'a> {
values: &'a EffectiveValues,
mode: CompileMode,
degradations: &'a mut Vec<Degradation>,
/// Targets of load-active authored-state overrides, best-effort only —
/// non-empty exactly when the construction pass found attributable
/// overrides. The walk leaves each out and declares it here, where the
/// stable path and document order are known.
override_skips: &'a HashMap<NodeId, String>,
nodes: Vec<FrameNode>,
/// The materialized nodes that are direct children of the root `<svg>`
/// — the animation inventory's candidate targets, which it narrows
Expand All @@ -1436,12 +1523,30 @@ impl ChildWalk<'_> {
*ordinal
};
let path = format!("{parent_path}/{tag}[{ordinal}]");
// The target of a load-active authored-state override: left out
// of the frame and declared here, where the walk knows its
// stable path — so override skips keep document order with
// every other skip. Strict never reaches this (it refuses the
// override at construction), and the reason names the animation
// element the inventory found.
if let Some(reason) = self.override_skips.get(&c.node_id()) {
self.degradations.push(Degradation {
path,
action: DegradationAction::Skipped,
reason: reason.clone(),
});
child = c.next_element_sibling();
continue;
}
// Non-rendering elements contribute no geometry and no hole:
// `<style>`'s CSS enters the one cascade (csscascade collects
// it), `<title>`/`<desc>`/`<metadata>` are descriptive text
// Chromium never paints, and animation elements contribute
// values. Declaring any of them would report a hole where the
// browser also draws nothing.
// it), and `<title>`/`<desc>`/`<metadata>` are descriptive text
// Chromium never paints. An animation element paints nothing
// *itself* either — but it is never silently inert: the
// animation inventory owns it, admitting the one sampled
// `<animate>` and turning every other one into a construction
// refusal (strict) or a declared skip of its target
// (best-effort, the override map above).
if is_non_rendering_element(&tag) || is_animation_element(&tag) {
child = c.next_element_sibling();
continue;
Expand Down Expand Up @@ -1521,7 +1626,7 @@ impl ChildWalk<'_> {
/// they change what referencing elements paint — skipping one silently
/// would change pixels — so they stay ordinary unsupported elements,
/// declared by name until the rung that consumes them.
fn is_non_rendering_element(tag: &str) -> bool {
pub(crate) fn is_non_rendering_element(tag: &str) -> bool {
matches!(tag, "style" | "title" | "desc" | "metadata")
}

Expand Down
Loading
Loading