Skip to content

fix: resolve document-level custom properties in the fail-closed visibility checker - #45

Merged
tw93 merged 3 commits into
tw93:mainfrom
jsiu93:fix/document-custom-property-resolution
Aug 23, 2026
Merged

fix: resolve document-level custom properties in the fail-closed visibility checker#45
tw93 merged 3 commits into
tw93:mainfrom
jsiu93:fix/document-custom-property-resolution

Conversation

@jsiu93

@jsiu93 jsiu93 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Fixes #44.

Summary

  • resolve var(--x) across declaration blocks in the fail-closed visibility checker when the document defines --x with one value everywhere (:root { --ink: ... } in one rule, body { color: var(--ink) } in another)
  • keep every existing fail-closed rule: a custom property declared with two different values anywhere in the document, an inline style="--x: ..." redefinition, or any @property registration still leaves the var() unresolved and the element ambiguous
  • add regression coverage: the :root case, every shipped template producing visible text under fail_closed=True, and adversarial documents that must not leak hidden text
  • regenerate the plugin mirror and the release archive

Why

_style_state parses one declaration block at a time and its resolved() helper only looks up var() inside that block. Every shipped template writes its tokens on :root and consumes them as bare var(--token) in later rules, so under fail_closed=True the checker marks body ambiguous and visible_html_evidence() returns no text at all. As a result python3 scripts/build.py --check-content content.json filled.html ends in "content coverage is indeterminate" (exit 2) for a document filled from any of the 27 templates in assets/templates/. Isolating the ambiguity per rule showed bare var() was the only source in all 27; the test suite never exercised the coverage gate against a shipped template, which is why this went unnoticed.

The fix adds a document-level custom property table with a deliberately narrow rule: a name is resolvable only when every declaration of it in the document (every rule body in every <style> block, plus every inline style attribute) normalizes to the same value. An undefined var() falls back to inherit/initial in the browser and cannot hide content on its own, so resolving to the one value the document ever gives it is either exact or conservative. It never turns hidden evidence into a coverage pass, which keeps the invariant stated in the _css_hidden_filters docstring.

Validation

  • python3 scripts/tests/test_build.py: Passed: 290 | Skipped: 0 | Failed: 0 (284 before this change; the new test adds 6 checks)
  • python3 scripts/build.py --check
  • python3 scripts/build_metadata.py --check
  • bash scripts/package-skill.sh dist/kami.zip: package audit passed (466,097 bytes)
  • python3 scripts/build.py --check-content <letter content> assets/templates/letter-en.html moves from "coverage is indeterminate" (exit 2) to reporting the unfilled placeholders as definitely missing (exit 1), which shows the checker now reads the template text
  • three CN templates (long-doc.html, portfolio.html, slides-weasy.html) still report the SVG <text> labels as indeterminate; that is the existing rule in _svg_position_state and is untouched here

…bility checker

`_style_state` only looked up `var()` inside the declaration block being
evaluated, so a template that sets its tokens on `:root` and consumes them
as bare `var(--token)` in later rules made `body` ambiguous under
`fail_closed=True`. `visible_html_evidence()` then returned no text and
`--check-content content.json filled.html` ended in "coverage is
indeterminate" for every shipped template.

Add a document-level custom property table with a narrow rule: a name is
resolvable across blocks only when every declaration of it in the document
(every rule body in every `<style>` block plus every inline `style`
attribute, collected through HTMLParser so unquoted and entity-encoded
values are seen) normalizes to the same value. Names declared with two
different values stay unresolved, and any `@property` registration disables
the table, so hidden evidence can never turn into a coverage pass. Local
declarations in the evaluated block still override the table.

Thread the table through `_css_hidden_filters`, the text evidence parser,
and the resource evidence parser in `content.py`. Add regression coverage:
the `:root` case, every shipped template producing visible text, and
adversarial documents (ancestor override, media redefinition, `@property`,
inline redefinition in three attribute syntaxes) that must not leak.
Regenerate the plugin mirror and the release archive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 18, 2026

Copy link
Copy Markdown

@jsiu93 is attempting to deploy a commit to the Faberon Team on Vercel.

A member of the Team first needs to authorize it.

@tw93
tw93 merged commit 3c4a2a9 into tw93:main Aug 23, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

--check-content coverage is always indeterminate for the shipped templates: fail-closed checker cannot resolve var() defined on :root

2 participants