Skip to content

web: HTML and CSS engine (0.2.0) - #1947

Merged
swapnilpaliwal-sd merged 133 commits into
apps/integration-0.2.0from
feat/web-layer
Oct 14, 2026
Merged

swapnilpaliwal-sd merged 133 commits into
apps/integration-0.2.0from
feat/web-layer

Conversation

@swapnilpaliwal-sd

@swapnilpaliwal-sd swapnilpaliwal-sd commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

The web (HTML + CSS) engine, the sixth language, released as 0.2.0. Merges into apps/integration-0.2.0, then dev for the 0.2.0 release. The parser goes to 0.2.1 (HTML and CSS front-end fixes on top of the 0.2.0 parser that shipped in 0.1.10).

What's in it

  • Engine: graph/web/ follows the other engines' layout (projections/, containment/, resolution/). Structural selector matching is Datalog (web_selector_match / web_selector_supported). Pages, stylesheets, @import, <style> blocks, inline style="", the cascade (specificity, layers, !important), custom properties across files, and links between pages are all covered. The web graph links HTML and CSS only; scripts and handlers stay as rows.
  • Engines on every platform: build-engines ships the web engine. Like every other engine it's parallel (-j 8, OpenMP), and a build fails if an engine lacks its .parallel marker.
  • Performance: style matching and var visibility no longer cost pages × selectors or uses × definitions. A 4,000-page site went from 202 s to 84 s with identical tables.
  • Verbs (CLI, MCP, plugin): impact on a class, id, var(--x), page, stylesheet, page.html:LINE or a selector; path from a page to a sheet or a class; context; impact and changed with no name answer for edited pages and stylesheets; tests reports that the web graph selects no tests.
  • Parser 0.2.1: HTML and CSS front-end fixes, and an opt-in --languages= filter. Without the flag, the other languages are parsed exactly as before.
  • Tests:
    • graph/test/web/ holds 134 cases. Each case is one .case file that tools/cases.py unpacks, byte for byte, at run time. That takes the suite from 692 files to 294.
    • Expectations come from an independent oracle (oracle/: parse5, postcss, css-select).
    • graph/test/ never ships: the npm package excludes it.
  • CI: the web suite is in the engine matrix. The e2e install check runs every web query verb from the installed tarball on all five platforms (.github/e2e/web, e2e-web-queries.sh).

Validation

  • Full CI dispatched on the branch (run 38509486472): all green. That covers the engines build and e2e on win32-x64, darwin-arm64, darwin-x64, linux-x64 and linux-arm64, plus all six language suites.
  • Local runs:
    • web suite 134/134;
    • oracle agreement 132/132;
    • CLI smoke 47/47;
    • violations 101/101;
    • includes 9/9.

Known, not blocking

  • Some controls are listed as "without a label" even though they don't need one: a button with text, or a hidden input.
  • impact .class repeats an element once for each rule that styles it, without saying which rule.
  • impact #id leaves out the element's inline style. impact page.html:LINE shows it.

🤖 Generated with Claude Code

graph/test/web/oracle/ derives the ground truth for every web node and
in-web edge kind from parse5, postcss, css-select and acorn (pinned in
its package.json; the axiom parser is never imported). run-tests.sh
builds each case's graph, normalizes the engine tables onto the oracle's
rows (contract in tools/normalize.py) and gates the multiset diff with
known-missing support; it fails with "engine not built" until
graph/web/engine exists. 51 hand-written cases across nodes, cascade,
refs, pages, js and crosslang, two generated scale cases; every
expectation was read row by row and agrees with the oracle (--oracle).

Refs #1908
…eyframe selector lists

Three corpus shapes the oracle-vs-IR comparison exposed: keyframes named
only through -webkit-/-moz-/-o-animation, theme sheets annotating every
value with a brace comment, and percentage lists in minified keyframes.
Declaration values are compared without comments; usemap, codebase and
profile join the URL attributes.

Refs #1908
…cle skips matching when no kind needs it

The suites live on web/torture while the engine is built on
feat/web-layer: AXIOM_ROOT points the runner at that checkout. The
oracle's matching stage now runs only for kinds that depend on it, so
the 2,000-page scale case takes 2 s in the oracle. The 200-page scale
case compares styles and cascade rows; var edges are left out (4.5M
rows on the generated site).

Refs #1908
Clearing the namespace of a universal node threw inside the selector
rewrite, so every selector containing `*` became selector_unparsed:
thousands of corpus selectors (`* html`, `> *`, `*::before`) had no
match rows. Found by listing the selector_unparsed partition on the dev
corpus; one invalid selector remains there, as it should. New case
cascade/17 covers both.

Refs #1908
…larations after a comment holding braces

The web walk skipped dist/, build/ and out/ by name (the source walks' list), so a
template's own compiled stylesheet, linked by every page, was never read. The web
walk now has its own skip list (EXCLUDED_DIRS less those three); javadoc/Dokka and
git-ignored directories are still skipped.

A comment holding a brace inside a declaration value (#ddd/*{borderColor}*/) turned
the rest of the block into bogus nested rules. Braces inside comments are blanked for
the grammar only; comment rows read their text from the source.

Refs #1909
…dies as JavaScript modules of their page

The HTML/CSS tables are also written to <ir>/web/ whenever a page or stylesheet was
read, so a repository of pages and stylesheets alone has IR for the web engine.

The JavaScript front end reads each page's inline classic/module <script> bodies and
on* attribute values as modules of their own: file = the page, qualified name
<page>#script-<n> / <page>#on-<n> (document order), text = the page with everything
else blanked so every line points into the .html file. A virtualKey keeps two modules
of one page apart in the module key; ordinary files keep their key.

parser/src/test/web-plumbing-tests.ts covers the web folder, the dist walk, brace
comments and the inline modules, each checked to fail without its fix.

Refs #1908
The oracle matched @scope rules unscoped as conditional at_rule:scope.
It now evaluates the scope: subjects under a root matching the start
selector and outside any limit below it, nearest root kept with its
proximity (new row kind `scope`, read from web_styles.scope_root and
scope_proximity); :scope and & name the root; a selector without them is
relative to it; a prelude-less @scope in a <style> is rooted at the
<style>'s parent; a non-exact bound gives scope_bound:<reason>. New case
cascade/18.

Refs #1908
…ow caps

normalize.py reads var rows from the view web_var(use, def, page,
status, reason) with use = the VARIABLE value_ref. tools/var_budget.py
fails when web_var_visible exceeds 4 x (var() uses + custom-property
declarations), web_var_scope exceeds 2 x the styles rows of var-using
rules or 2M, web_var is materialized, or there is no var() use to
measure. Wired as check=var_budget on the three var cases and on the
200-page scale case, which compares the view's count with the oracle's
4.5M rows again.

Refs #1908
…arent list, disabled sheets

Per the iter1b rulings: an element whose id or attribute is bound at run
time (:id, x-bind:data-state, [attr.aria-expanded], id="row-{{ n }}")
gets one unknown dynamic_attribute row for each selector the binding may
complete (dynamic_attribute;dynamic_class when it needs both kinds), never
a match; & under '#app, .shell' is 1,1,0 for both; <link disabled> and
alternate sheets load conditional alternate_sheet. Cases cascade/19, 20.

Refs #1908
…sing

G10 refs/09 (vendor animation shorthands), G11 nodes/04 now loads a
sheet that targets the iframe fallback markup (never styled; the
normalizer drops elements with inert=iframe_text and reports a styles
row on one as an extra), G12 nodes/18 compares selectors (keyframe
selectors are absent), G13 refs/05, G14 pages/05 (a site inside a
sub-project with its own package.json; keys stay repo-relative).
No known-missing file exists.

Refs #1908
…I's --json

cases/queries/01-admin-site is a hand-written dashboard site with 38
questions (37 graded, Q36 skipped_not_built until fragment hosts exist)
over context, impact, path and link. tools/query_oracle.py derives every
answer from the oracle's rows (expected/queries/..., each read by hand);
tools/grade_query.py grades SET, SET>=, ORDER, CHAIN and TOP-k against the
SPEC 6.2 row contract (at, role, status, rank; the JavaScript rows under
other_languages.javascript), re-asks with --limit 0 when the answer says
more > 0, and never counts a skipped template as correct. The runner
prints query_ok per case; --oracle checks every derived answer still
matches its reviewed expectation.

Refs #1908
…ic attributes, written type names

The bundle's web tables follow graph/test/web/tools/normalize.py (uid/file/line/col,
element_uid, ...). Selector matching: static reasons per selector, initial-state
pseudo-classes true only when the attributes say so, whole-selector unknowns
(shadow_dom, column_combinator, selector_unparsed, implied_element) as one unknown
row per page, dynamic class/attribute bindings as unknown rows, @scope as exact
containment with scope_root/scope_proximity, HTML case-insensitive attribute values,
SVG type names taken back as written (the parser lowercases them). Layer ranks and
load order per SPEC 3.3; alternate/disabled sheets conditional. var edges stored as
web_var_def/web_var_visible/web_var_scope with web_var a SQL view.

Refs #1908
…kept it out)

The repository-wide `oracle/` and `package-lock.json` ignore rules
matched graph/test/web/oracle, so no commit on this branch carried the
ground truth the suites are generated from. graph/test/web/.gitignore
re-includes oracle/ and its lockfile (an exception to the no-lockfile
rule: the oracle is a pinned measuring instrument, its transitive
versions included) and keeps oracle/node_modules/ out.

Refs #1908
… block with its whole list

The grammar rejects the list's leading items as an ERROR inside the keyframe block list;
the recovery that reads them as the next block's prelude never saw it, because the
ERROR was lifted into its parts. Kept whole there now.

Refs #1908
…nts, unknown rows only for undecidable selectors

A trailing slash on a non-void HTML element is ignored by HTML; the following siblings
become its content (the parser closes it at the slash). Declaration values drop their
comments. web_unknown carries the selector/sheet/page unknowns; id references, loads and
parse gaps keep their own tables.

Refs #1908
…refs, var scope rows per (element, name)

Parser: `@keyframes "fade"` is an @Keyframes named fade (the grammar takes only an
identifier); the parser torture expectation that encoded the gap is updated.
Engine: a link to `docs/` reaches docs/index.html; -webkit-/-moz-/-o-animation names
become value references of their own (G10); web_var_scope holds one row per
(page, element, name) and only for names something defines.

Refs #1908
…r HTML and CSS (#1909); styled_by questions grade their own rows

The web walk no longer skips dist/, build/ and out/ by name (the
JavaScript walk still does), so refs/08 now loads dist/app.css as a
match at load order 1 and the later links shift by one; a link into
target/ keeps the not_indexed shape. No other expectation depended on
the old walk.

Q1, Q23, Q24 and Q28 all read the styled_by rows of one element; each
expectation now names its filter (exact non-important / has conditions /
state:* / important_count > 0) and grade_query.py applies it before
grading. grade_query_selftest.py (run by --oracle) shows one full
answer satisfies all four and that a dropped, corrupted or reordered
row fails the question it belongs to.

Refs #1908
…e SPEC 6.2 row contract

ax_web.py answers impact/path/context on a web graph (the verb scripts hand it a graph
whose run.language is web): rows {at, kind, role, status, reason, rank, ...} for
classes, ids, custom properties, pages, stylesheets, elements (page.html:LINE, the
cascade in §3.3 order), selectors, @Keyframes, @font-face, @layer, @container and
inline scripts; path over links, loads/@import, rules, styles and var(); context ranks
the files a task's words land on. tests/changed exit 3 (nothing to say). The front
door prints the web prose after the other languages' places. axiomcode-index indexes a
web graph's names as symbols (INDEX_VERSION 9); axiomcode-build counts pages and
stylesheets (web is the main language only when nothing else has files) and adds the
javascript graph for pages with inline scripts.

Refs #1908
…he page's markup

Read from the JavaScript IR in the bundle (API name, receiver shape, literal argument):
querySelector/All, getElementById, getElementsBy*, closest, matches, *Attribute,
add/removeEventListener, createElement, insertAdjacentHTML, classList.*, style.setProperty,
className=/innerHTML=/outerHTML=/textContent=/on<event>=/style.<prop>=, dataset.<k>, and
jQuery's $()/jQuery() and its class, attribute, css, event and traversal methods (only on a
jQuery receiver). Literal tokens split (.a #b div [data-x]); a non-literal argument is a
non_literal row. Bundled/minified modules are not listed. No row names a web node.
Kept out of the Soufflé rules: it needs no resolution and would cost a cold JavaScript
engine compile per change. In a fanned-out --json answer the web graph's rows lead and
the JavaScript graph's sit under other_languages.javascript (SPEC 6.2).

Refs #1908
…is known by; nameless on* calls are not handler rows

Refs #1908
…age, a .class/#id or a .js file answers with inline-script functions and DOM touches

ext_module_provenance(module, file, qualified_name, provenance) and
ext_method_module(method_id, module). On a JavaScript graph, impact <page.html> lists the
functions of the page's inline scripts and on* bodies (role inline_script, named by
ext_method_name) and its DOM touches; impact <.class|#id> the DOM touches whose tokens
name it; impact <file.js> the usual answer plus its DOM touches. Rows of the JavaScript
graph only; the fan-out puts them under other_languages.javascript.

Refs #1908
… kept as BUNDLED, acorn-loose recovery

Per the js rulings: normalize.py names a function COALESCE(ext_method_name.name,
methods.name) and adds a provenance column from ext_module_provenance
(PROJECT when a module has no row). The oracle keeps a site's own
minified functions with provenance BUNDLED (their DOM touches stay
outside the recall set) and falls back to acorn-loose 8.5.2 (pinned)
when acorn rejects a script, so js/02's broken declaration is expected on
both sides with no known-missing entry. New js/07: two inline scripts
recovery can find no function in, beside a working one, so an invented
function fails as an extra and the case still compares rows. js/01-03
and js/06 re-blessed (provenance column; js/06 gains its .min.js
function).

Refs #1908
…L and CSS only)

Reverts the G6 part of ae6c6d1a: the JavaScript front end no longer reads a page's
inline <script> bodies and on* values as modules, the module key's virtualKey and the
HTML parser's scriptSpans are gone, and the plumbing test for it is removed. The
JavaScript parser, the module registry and the HTML parser are byte-identical to
3702fa6f again. The <ir>/web/ folder (G5) stays.

Refs #1908
…nswers (scope: HTML and CSS only)

Removes ext_dom_touch, ext_method_name, ext_module_provenance and ext_method_module from
the JavaScript bundle (graph/bundle/dom-touch.ts), the JavaScript-graph impact on pages,
.class/#id and .js files, and the web-first order of the fanned-out --json answer
(reverting 39396eb2, aec9e2c2, f8924df8, b9d08f11, 381f2550 and that part of 54f127b9),
and the build's javascript graph for pages with inline scripts. The web graph keeps a
<script> as markup (src, resolved file, type, body range, body line count, inline_index)
and a handler call by the name written (handler_index); nothing reads script text as
JavaScript.

Refs #1908
swapnilpaliwal-sd and others added 22 commits October 12, 2026 22:50
- Single-quoted values: the template-quote mask ran from a stray {{ in a
  script to a later }} and unquoted every single-quoted attribute between,
  so class='a b' split at the space. Template tags are now one line and at
  most 500 characters (nodes/26).
- V4-07: a directory URL without the trailing slash resolves to its index
  file in reference.resolved_file and links_to (pages/10).
- V4-03: javascript:, javascript:;, void(0) and void 0 are no-op links with
  no handler row (handlers/19; handlers/02 re-blessed).
- V4-08: the walk skips only node_modules/, .git/ and the project's
  .gitignore files, so target/, env/ and dot directories are read
  (pages/09; refs/08 re-blessed for target/site.css).
- V4-01: every SSI directive is a row wherever it sits, top of file and
  inside attribute values included (includes/15).
- V4-05: value_text and prelude_text are stored with comments removed and
  trimmed, bytes counted on that text (nodes/27).
- V4-02: an <include ... /> parses as an open element; the fragment takes
  its place and what parsed inside it follows as siblings (includes/16).

Refs #1908
The CR inside a value is what the case tests. Without -text, git stored
the file with LF line endings and the case would compare another file
than the one reviewed.

Refs #1908
…30)

A duplicated answer row was absorbed by set comparison and passed. SET
now requires the exact multiset; SET>= requires every expected tuple at
least as often as expected and no tuple more often. The grader names the
duplicated rows. Self-test 25/25: the exact multiset passes, a duplicate
fails under both grades, and a tuple expected twice (two nodes on one line)
passes when answered twice. On the impl, queries/01 now fails Q6 and Q12 on
duplicate rows (listed in learnings.md); queries/02 and 03 pass.

Refs #1908
… rule_at, status

Ruling (iter5 orchestrator): a selector-to-element answer is one row per
(element, rule), so two rules styling one h2 are two correct rows. Q6, Q36,
Q36c and Q51 now grade at,rule_at,status (Q51 with host). The expectation
holds each rule's best status on the element, a rule counted once when two
of its selectors match. The grader compares every *_at field as file:line.
styled_by questions already name the rule in at and are unchanged. Q12
stays as it is (one handler row per attribute, with calls).

Refs #1908
…ies, comment-free names, the web walk's skips

The iteration-5 fixes on the parser side; their checks (violations.sh V4, in the next commit) fail at 6383fd9:
- V4-01: every SSI directive is a row wherever it sits: at the top of a file (no owner element), several in
  one attribute value, and #comment. An SSI directive inside a quoted attribute value is opaque in the
  start tag (its own quotes end nothing) and keeps the value whole.
- V4-04: a url() body (a data: URI) is kept whole below 64 KiB in its value reference (V2-12), not cut at
  1 KiB.
- V4-05: a declaration's value and a rule's prelude keep a carriage return as written (EntityUtils.escapeTsv,
  which every front end shares, folds CRLF; the web rows use their own helper).
- V4-06: font-family, font and animation names are read with comments blanked, so `Arial/*{ff}*/` is a
  FONT_FAMILY reference.
- V4-08 ruling: the web walk skips only node_modules/, .git/ and what the repository's ignore files exclude;
  target/, env/ and dot-directories are read. The JavaScript and TypeScript walks keep EXCLUDED_DIRS.

Refs #1909
…t byte counts; one handler row, styled rows per rule

The iteration-5 fixes on the engine and query side, each with a check that fails at 6383fd9:
- V4-02: the `<div/>` repair no longer takes `<include … />` (a build-time directive the include tool closes):
  what follows it stayed its children and vanished when the fragment replaced it, so a later include's
  fragment had no styles and no computed rows (V3-05 on r5 too).
- V4-03 ruling: `javascript:`, `javascript:;` and `javascript:void(0)` / `void 0` are no-op links, no handler row.
- V4-05 ruling: value_bytes and prelude_bytes are the UTF-8 length of value_text and prelude_text as stored
  (comments removed, edges trimmed; a value cut at 64 KiB keeps the length of the whole value written), and
  prelude_text is stored comment-free.
- [iter5 ruling] the `handler` answer role is one row per on* attribute or javascript: URL, the functions it
  calls in `calls`; a template event directive keeps a template_expr row per call.
- [iter5 ruling] element-styled answers (`styled`) are one row per (element, rule), per host for a composed
  fragment, with `rule_at` and the rule's best status on that element; a rule whose two selectors both match
  counts once.
V4-07 needed no engine change: its check passes at 6383fd9 (the oracle was behind).

Checks: violations.sh V4-01..V4-08, cli-smoke V5-handler-one-row and V5-styled-per-rule.

Refs #1908
…ry css url() is a resource row

iter4 ruling: a relative URL inside an included fragment resolves against the HOST page's location in each
composed page. Its link href and element src already did (a8cc043); a url() in the fragment's own <style>
or in a style attribute inside it still resolved against the fragment file. Each css url() now gives a
web_resources row (from_kind declaration, kind URL), owned by what it resolves against: the stylesheet for a
sheet file, the page for a <style> or style attribute, and, for an included fragment, one row per host
resolved against that host's location in place of the fragment-relative one. web_value_refs keeps the url()
as written, resolved against its own file.

Check: violations.sh VIA-04 (a fragment's <style> url(), quoted and root-relative, and a style-attribute
url(), under two hosts in different directories, beside a page's and a sheet file's own); fails at 78cac39.

Refs #1908
Review 1 #23, third bullet. With --src the web graph's page paths are relative to the source directory, while
axiomcode-web-links.tsv sits at the repository root with repository-relative paths, so:
- web_verdicts keyed the host by its source-relative path and never matched the link it was asked about: it
  now puts page paths back under run.source_dir before keying, as the links file writes them;
- the file table watched only the source directory, so the links file written at the root never made the
  graph stale and `link` rebuilt nothing: the root links file is watched as well when --src leaves it outside.

Checks: cli-smoke R1-23-src-link-taken, R1-23-src-link-list (index --lang web --src site, then link and the
listing); both fail at 78cac39, and each still fails with only one of the two changes.

Refs #1908
…other; fail a build whose engine has no .parallel marker

The web branch adds a Soufflé program (graph/web) that run-souffle.sh looks up in the
platform engine package, but build-engines never built it: a user without Soufflé would
get 'has no web engine' and no web graph. web joins LANGUAGES and the Windows loop, so it
is generated with -j 8, compiled with OpenMP on every platform and marked .parallel.

The smoke step now fails when an engine lacks its .parallel marker (run-souffle.sh would
solve it at -j 1) and runs each engine at -j 8, as a user's solve does.

web stays out of ci.yml's install-and-run loop: that check counts call edges and runs the
query verbs, and web has neither.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… x selectors and uses x definitions

Two loops in the web bundle were quadratic in the site, not in its answers:
- every page visited every selector of every sheet it loads, though most require a class
  or id the page does not carry (4,000 pages x 10,000 loaded rules = 40M visits);
- every var() use walked every definition of its name (20k uses x ~1,800 defs).

Selectors: each sheet is indexed once by a class or id its selectors require (a browser's
rule hash). A plain page (one run, no composition, no dynamic class binding) visits the
selectors whose key it carries plus those with no key, in sheet order; a skipped selector
can only be tallied as loading, unmatched and missing on that page, and those tallies are
added in bulk. Undecidable, :root and @scope selectors are never skipped, and a composed,
extended or dynamic page still visits every selector.

Vars: a name's definitions are grouped by owner once; a use walks owners, and whether a
use sheet and a definition sheet share a page is asked once per pair.

Output unchanged: every table identical (sorted rows) on all 132 web suite cases and on a
generated 1,000- and 4,000-page site; the web suite passes 132/132. Bundle on the
1,000-page site 101 s -> 25 s (styles 39 s -> 10.8 s); 4,000 pages 202 s -> 84 s end to end.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…touched it)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dl), like every other engine's resolution

HTML <-> CSS linking moves toward the engine: element tokens and the tree, a selector's compounds
joined right to left (type, class, id; descendant and child), solved by the engine at -j 8 and
exported as web_selector_match(selector, page, element) and web_selector_supported(selector) —
relations a query or a pack can join, written to the graph as ext_web_selector_*.

The bundle reads the engine's matches where they are exact: a plain page (no composition, no
class binding) with no quirks mode, no mended <div/>, no upper-case HTML tag, and — for a selector
with a type part — no SVG or MathML element (the parser lower-cases a type selector's name). Every
other page and selector keeps the bundle's matcher, which also still turns each match into a
status, the cascade and computed style.

Output unchanged: every existing table identical on all 132 web suite cases (57 of them take
matches from the engine) and on the generated 1,000- and 4,000-page sites.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nt, resolution; tests under graph/test/web

- engine/projections/elements.dl, selectors.dl: column reads of the HTML and CSS facts
- engine/containment/ownership.dl: rules nested in a style rule
- engine/resolution/loads.dl (+ web_page_sheet), selectors.dl (was match.dl), values.dl;
  headers in the engines' 'Resolution · NAME' form, and loads.dl no longer says selector
  matching is the bundle's
- graph/web/test/{cli-smoke,includes,violations}.sh move to graph/test/web/, beside the suite,
  as every language keeps its tests; bin/axiomcode follows
- parser/tools/web-corpus-bench/BASELINE-2026-10-07.md removed: a dated, generated snapshot

Unchanged output: every table identical on all 132 web cases; web suite 132/132, cli smoke 36/36,
include checks 9/9, violation checks 101/101.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, its CLI/violation/include checks, its declarations against the parser schema

- the suite matrix gets a web leg: graph/test/web/run-tests.sh (132 oracle-gated cases), then
  graph/test/web/{cli-smoke,violations,includes}.sh; AXIOMCODE_ENGINE points the front door
  (the web query cases) at this checkout; tests/run.py has no web cases, so that step skips web
- web joins the decls_base drift check; it stays out of check_staging (it stages no library)
- run-tests.sh measured a build's peak memory with BSD 'time -l', which fails every case on
  Linux: it now uses GNU 'time -v' there (kB) and 'time -l' on macOS (bytes), and the leg
  installs 'time'

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… check and the nightly

e2e-install.sh checked a call graph's edges and the call-graph verbs, which a web graph has
neither of: for web it now checks that the installed package used its packaged web engine and
wrote pages, selectors, style rows and engine matches. ci.yml and nightly.yml run it on a web
fixture (cases sit one level deeper) whose selectors the engine supports.

Checked locally: packed tarballs installed into an empty project, souffle hidden, packaged
darwin-arm64 web engine used; 1 page, 6 selectors, 12 style rows, 7 engine matches.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts; path reaches a class only selectors name

- impact (no name), CLI and MCP: the web graph maps each hunk of an edited .html/.css to the element or rule it is
  in and answers for those, beside the call graphs' answer (text and --json), against the same baseline `changed`
  uses; it says when the graph predates the edit (by content, ax_fresh's file table). Before: "your edits: none".
- path: a .class or #id endpoint is the elements carrying it and the selectors naming it, so `path page.html .x`
  reaches a rule for a class no element carries instead of "nothing named '.x'".
- context: a page named by a number or a file name ("the 404 page") is found.
- changed / tests: pages and stylesheets are named as the web graph's, not as files outside every indexed
  language; tests answers in text when the JSON answer has no places (it printed the raw document).
- index: the web graph's summary line counts pages, stylesheets, selectors and styled elements (it was empty).

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nswers), 692 files to 294

A case's site was a directory of a case.conf and a few small pages and stylesheets; it is now one text file the runner
unpacks into .work/cases/ (tools/cases.py, byte-exact: binary or CR files go base64, a file without a final newline is
marked). The engine and oracle runs read the unpacked tree exactly as before; CI and the nightly unpack their web case.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…too (.github/e2e/web)

The install check stopped at "the web graph has rows" for web. e2e-web-queries.sh is e2e-queries.sh's twin for a site:
impact on a class, a stylesheet and a page, path page -> imported sheet, context, --json, the same three verbs where
exec does not replace the process (#1640), then an edit to a rule (impact with no name) and tests (exit 3, none).

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@swapnilpaliwal-sd swapnilpaliwal-sd changed the title web: HTML and CSS engine (draft: first full CI run) web: HTML and CSS engine (0.2.0) Oct 14, 2026
@swapnilpaliwal-sd
swapnilpaliwal-sd changed the base branch from dev to apps/integration-0.2.0 October 14, 2026 05:31
@swapnilpaliwal-sd
swapnilpaliwal-sd marked this pull request as ready for review October 14, 2026 05:31
@swapnilpaliwal-sd
swapnilpaliwal-sd merged commit 8caa449 into apps/integration-0.2.0 Oct 14, 2026
13 checks passed
@swapnilpaliwal-sd
swapnilpaliwal-sd deleted the feat/web-layer branch October 14, 2026 05:49
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.

1 participant