[PR #1683] [MERGED] fix(browser): resolve XLink and SVG prefixes in XHTML dom_urls #4752

Closed
opened 2026-10-31 00:54:41 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/1683
Author: @sathariels
Created: 10/11/2026
Status: ✅ Merged
Merged: 10/11/2026
Merged by: @morluto

Base: main ← Head: fix/dom-urls-xhtml-svg-prefix


📝 Commits (2)

  • 97ccfa0 fix(browser): resolve XLink and SVG prefixes in XHTML dom_urls
  • 20a38e1 fix(browser): bound XML namespace resolution and reject cyclic ancestry

📊 Changes

4 files changed (+872 additions, -62 deletions)

View changed files

📝 src/browser/CdpCaptureDocuments.baseUrl.test.ts (+399 -0)
📝 src/browser/CdpCaptureDocuments.ts (+40 -60)
➕ src/browser/CdpCaptureDomSvg.ts (+275 -0)
📝 tests/boundary/browser/cdpSvgXlinkHref.test.ts (+158 -2)

📄 Description

Summary

inspect-web-page still omits dom_urls entries for some SVG links in XHTML. There is no linked issue for this change. It is the follow-up CassieFox23 reported on https://github.com/morluto/rea/pull/1602#issuecomment-6100254993 after #1602 merged: a prefixed <svg:a xlink:href> in an XHTML page is a link in Chrome and still gets no entry. This resolves element and attribute prefixes from in-scope xmlns declarations in XML documents only. HTML documents are unchanged. Refs #1586.

Problem and expected behavior

#1602 reports a literal xlink:href when an element or ancestor is named svg and a closer ancestor is not foreignObject. Chrome's XML parser keeps prefixes, so an XHTML document's nodes are html, svg:svg, and svg:a, and none of those names match svg.

Smallest trigger: an XHTML page (application/xhtml+xml) containing <svg:a xlink:href="/xl-pa1">. metadata.dom_urls has no entry. Chrome treats that node as a link (namespaceURI is the SVG namespace, href.baseVal is set, and it matches :any-link).

Expected, still stored as attribute href, with the same base URL and origin policy as a plain href:

  • <svg:a xlink:href> in XHTML, including one nested under svg:foreignObject
  • <svg:a xlink:href> with no svg parent, when the svg prefix is bound to the SVG namespace
  • <svg:image xlink:href>
  • an unprefixed SVG <a p:href> when p is any prefix bound to http://www.w3.org/1999/xlink

Plain href wins even when it is empty. The first duplicate attribute wins. These stay unreported: an XHTML <a> or <h:a> inside foreignObject whose namespace is XHTML, and <bogus:svg> whose prefix is bound to a namespace other than SVG. HTML documents keep today's literal xlink:href behavior.

Change and scope

DOMSnapshot.captureSnapshot still has no namespace field. It does keep xmlns and xmlns:p as attributes. HTML documents show an uppercase document element (HTML); XHTML and other XML documents show lowercase html or a prefixed root.

HTML documents (document element HTML, and the existing synthetic snapshots whose root is unprefixed svg) still use the per-document ancestry cache: SVG context is the name svg, stopped by foreignObject. The fallback attribute remains the literal name xlink:href.

XML documents resolve prefixes in one walk and store the result in that same per-document cache. Each node reuses its parent's namespace scope when it has no local xmlns declaration, so a deep tree does not walk ancestors again and does not copy a map per level. A cyclic parent chain fails closed. The default namespace applies to unprefixed elements and not to unprefixed attributes. An element is SVG only when its resolved namespace is http://www.w3.org/2000/svg. The fallback is the first p:href whose prefix resolves to http://www.w3.org/1999/xlink. It is used only when plain href is absent, and it is reported as href. Node names and attribute names are not rewritten.

Intentionally not included:

  • A schema or dom_urls[].attribute change. browserObservationSchemas.ts is unchanged.
  • MathML, a tag whitelist, or stripping prefixes.
  • Treating a standalone unprefixed svg root with no xmlns as XML. Those snapshots stay on the HTML-parser path so #1602's tests stay valid.

Contract and boundary impact

  • Semantic owner and earliest changed stage: domMetadata in the browser CDP capture adapter, while normalizing a DOMSnapshot.captureSnapshot result into metadata.dom_urls. Prefix resolution is owned by src/browser/CdpCaptureDomSvg.ts at that same stage. No earlier capture stage changes.
  • CLI and MCP/tool-catalog contract: none. The dom_urls[].attribute enum stays href | src | action | formaction | poster. XHTML pages that previously omitted prefixed SVG links now include those URLs as href.
  • Provider, bridge, target-format, or platform compatibility: Chrome-family CDP DOM snapshots only. No bridge or target-format change. XML detection uses Chrome's document-element casing (HTML vs html) and xmlns attributes preserved on the snapshot.
  • Evidence, artifact, provenance, or reconstruction contract: none. Added entries are the same derived URL records as existing href values (resolved URL plus destination_scope). Observed node names and attribute names stay prefixed (svg:a, xlink:href).
  • Process execution, authorization, cleanup, or containment impact: none. Destination policy and URL sanitization are unchanged. The new boundary test launches a local headless Chrome only when an executable is already installed.
  • Generated metadata (docs/public/product-catalog.json), package, or installation impact (CI generates and validates ignored catalogs and skills/; edit authored instructions in skill-src/ and never commit build outputs): none. No schema, catalog, package, or skill source change.

Evidence and regression coverage

Executed against upstream/main (3749ccec) with the new tests applied and the production change reverted, then again with the change.

  • Tests added or updated: src/browser/CdpCaptureDocuments.baseUrl.test.ts (prefixed svg:a and svg:image, orphan svg:a, svg:a under svg:foreignObject, XHTML <a> and <h:a> excluded, custom XLink prefix, literal xlink rebound to another namespace, href wins including empty href, outside-policy origin, bogus:svg, HTML document unchanged, xmlns on the element itself, forward ancestor, 4000 nested elements under 1000ms). tests/boundary/browser/cdpSvgXlinkHref.test.ts (real headless Chrome, page served as application/xhtml+xml).
  • Base reproduction or other evidence: on 3749ccec, npm run test:focused -- src/browser/CdpCaptureDocuments.baseUrl.test.ts tests/boundary/browser/cdpSvgXlinkHref.test.ts failed 9 tests and passed 36. The XHTML Chrome test received 2 href entries (the plain href and the empty href) and not the 8 entries the page should produce. Prefixed snapshot fixtures produced [] instead of the SVG URL. The custom-prefix fixture reported https://app.example.test/not-xlink from the literal name xlink:href instead of https://app.example.test/xl-custom. The HTML-document test and the plain-href precedence test passed on that base because plain href is already collected for every element and HTML names are unchanged.
  • User-visible CLI/MCP output (if applicable): the headless Chrome boundary test reads metadata.dom_urls from CdpBrowserProvider.inspectPage, the same records the CLI and MCP adapters return. For <svg:a xlink:href="/xl-pa1"> the entry is attribute href, url {origin}/xl-pa1, destination_scope approved. The DOM node stays svg:a with attribute name xlink:href. The CLI and MCP routes were not invoked.
  • Remaining proof gaps: Firefox, Safari, and Chromium builds other than the installed Google Chrome were not run. The full verify:browser lane, inspect-web-page CLI, and MCP route were not run. No real-world SVG page beyond the fixture.

For evidence-bearing changes:

  • Observed, derived, and inferred claims remain distinguishable.
  • Artifact identity, source provenance, and failed attempts remain preserved.
  • Unsupported, incomplete, unavailable, or uncertain outcomes remain visible.

Validation performed

  • npm ci (Node v24.18.0, npm 11.16.0) - passed.
  • npm run format:check on unmodified upstream/main 3749ccec - passed (All matched files use the correct format, 2491 files). The earlier unrelated format failure is not present on this base.
  • npm run test:focused -- src/browser/CdpCaptureDocuments.baseUrl.test.ts tests/boundary/browser/cdpSvgXlinkHref.test.ts on that base with these tests - failed, 9 failed / 36 passed. REA_BROWSER_NO_SANDBOX=true.
  • Same focused command on this branch - passed, 45 tests. Unit file about 25ms, including the 4000-element case. Boundary file 2 tests, about 443ms. Google Chrome 148.0.7778.96.
  • npm run check - passed. Turbo typecheck, lint, format:check, knip, and metadata:check succeeded. Lint reported 707 warnings and 0 errors in unrelated files. Architecture guards passed across 1110 source files. format:check passed on this branch as well (All matched files use the correct format, 2492 files).
  • npm run verify:test-discovery - passed (Discovered all 811 test files exactly once).
  • Pre-commit lint-staged (oxfmt, oxlint) and pre-push npm run check:fast - passed on push.

Compatibility, safety, and release

  • Breaking changes or migration steps: none. HTML dom_urls output is unchanged. XHTML pages gain href entries that were previously missing. Callers that assumed those links were absent will now see them, with the same attribute enum and destination policy.
  • Real Hopper/Ghidra, browser, or OS coverage: headless Google Chrome 148.0.7778.96 on Linux, sandbox disabled via REA_BROWSER_NO_SANDBOX=true because this environment requires it. The test skips when no Chrome-family executable is installed. Hopper and Ghidra are not involved.
  • Package or release metadata impact: none. No version bump, changelog edit, or generated catalog commit. Release Please will pick up the conventional commit subject.
  • Security, privacy, process, or containment review: destination policy, URL sanitization, and allowed origins are unchanged. Outside-policy and non-http XLink values stay outside_policy or unsupported. No new process authority. The boundary test starts a local Chrome and a loopback fixture server and tears both down.

Review checklist

  • This PR addresses a concrete problem or an agreed enhancement.
  • The PR has one focused outcome and the title follows type(scope): outcome.
  • Related issue is linked, or the reason for not linking one is stated above.
  • Tests cover changed observable behavior and meaningful failure paths.
  • Owning docs, contracts, and generated metadata are updated where needed.
  • User-visible CLI/MCP changes include representative output.
  • I checked the final diff for secrets, unrelated cleanup, and unsupported claims.

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/morluto/rea/pull/1683 **Author:** [@sathariels](https://github.com/sathariels) **Created:** 10/11/2026 **Status:** ✅ Merged **Merged:** 10/11/2026 **Merged by:** [@morluto](https://github.com/morluto) **Base:** `main` ← **Head:** `fix/dom-urls-xhtml-svg-prefix` --- ### 📝 Commits (2) - [`97ccfa0`](https://github.com/morluto/rea/commit/97ccfa083950397621ca40115bfeaa8c1c06cd2f) fix(browser): resolve XLink and SVG prefixes in XHTML dom_urls - [`20a38e1`](https://github.com/morluto/rea/commit/20a38e1914dbe4f739a672a16f06d23182cbc8bf) fix(browser): bound XML namespace resolution and reject cyclic ancestry ### 📊 Changes **4 files changed** (+872 additions, -62 deletions) <details> <summary>View changed files</summary> 📝 `src/browser/CdpCaptureDocuments.baseUrl.test.ts` (+399 -0) 📝 `src/browser/CdpCaptureDocuments.ts` (+40 -60) ➕ `src/browser/CdpCaptureDomSvg.ts` (+275 -0) 📝 `tests/boundary/browser/cdpSvgXlinkHref.test.ts` (+158 -2) </details> ### 📄 Description <!-- PR title: fix(browser): resolve XLink and SVG prefixes in XHTML dom_urls The title becomes the squash-merge commit subject and generated changelog entry. --> ## Summary `inspect-web-page` still omits `dom_urls` entries for some SVG links in XHTML. There is no linked issue for this change. It is the follow-up CassieFox23 reported on https://github.com/morluto/rea/pull/1602#issuecomment-6100254993 after #1602 merged: a prefixed `<svg:a xlink:href>` in an XHTML page is a link in Chrome and still gets no entry. This resolves element and attribute prefixes from in-scope `xmlns` declarations in XML documents only. HTML documents are unchanged. Refs #1586. ## Problem and expected behavior #1602 reports a literal `xlink:href` when an element or ancestor is named `svg` and a closer ancestor is not `foreignObject`. Chrome's XML parser keeps prefixes, so an XHTML document's nodes are `html`, `svg:svg`, and `svg:a`, and none of those names match `svg`. Smallest trigger: an XHTML page (`application/xhtml+xml`) containing `<svg:a xlink:href="/xl-pa1">`. `metadata.dom_urls` has no entry. Chrome treats that node as a link (`namespaceURI` is the SVG namespace, `href.baseVal` is set, and it matches `:any-link`). Expected, still stored as attribute `href`, with the same base URL and origin policy as a plain `href`: - `<svg:a xlink:href>` in XHTML, including one nested under `svg:foreignObject` - `<svg:a xlink:href>` with no `svg` parent, when the `svg` prefix is bound to the SVG namespace - `<svg:image xlink:href>` - an unprefixed SVG `<a p:href>` when `p` is any prefix bound to `http://www.w3.org/1999/xlink` Plain `href` wins even when it is empty. The first duplicate attribute wins. These stay unreported: an XHTML `<a>` or `<h:a>` inside `foreignObject` whose namespace is XHTML, and `<bogus:svg>` whose prefix is bound to a namespace other than SVG. HTML documents keep today's literal `xlink:href` behavior. ## Change and scope `DOMSnapshot.captureSnapshot` still has no namespace field. It does keep `xmlns` and `xmlns:p` as attributes. HTML documents show an uppercase document element (`HTML`); XHTML and other XML documents show lowercase `html` or a prefixed root. HTML documents (document element `HTML`, and the existing synthetic snapshots whose root is unprefixed `svg`) still use the per-document ancestry cache: SVG context is the name `svg`, stopped by `foreignObject`. The fallback attribute remains the literal name `xlink:href`. XML documents resolve prefixes in one walk and store the result in that same per-document cache. Each node reuses its parent's namespace scope when it has no local `xmlns` declaration, so a deep tree does not walk ancestors again and does not copy a map per level. A cyclic parent chain fails closed. The default namespace applies to unprefixed elements and not to unprefixed attributes. An element is SVG only when its resolved namespace is `http://www.w3.org/2000/svg`. The fallback is the first `p:href` whose prefix resolves to `http://www.w3.org/1999/xlink`. It is used only when plain `href` is absent, and it is reported as `href`. Node names and attribute names are not rewritten. Intentionally not included: - A schema or `dom_urls[].attribute` change. `browserObservationSchemas.ts` is unchanged. - MathML, a tag whitelist, or stripping prefixes. - Treating a standalone unprefixed `svg` root with no `xmlns` as XML. Those snapshots stay on the HTML-parser path so #1602's tests stay valid. ## Contract and boundary impact - Semantic owner and earliest changed stage: `domMetadata` in the browser CDP capture adapter, while normalizing a `DOMSnapshot.captureSnapshot` result into `metadata.dom_urls`. Prefix resolution is owned by `src/browser/CdpCaptureDomSvg.ts` at that same stage. No earlier capture stage changes. - CLI and MCP/tool-catalog contract: none. The `dom_urls[].attribute` enum stays `href | src | action | formaction | poster`. XHTML pages that previously omitted prefixed SVG links now include those URLs as `href`. - Provider, bridge, target-format, or platform compatibility: Chrome-family CDP DOM snapshots only. No bridge or target-format change. XML detection uses Chrome's document-element casing (`HTML` vs `html`) and `xmlns` attributes preserved on the snapshot. - Evidence, artifact, provenance, or reconstruction contract: none. Added entries are the same derived URL records as existing `href` values (resolved URL plus `destination_scope`). Observed node names and attribute names stay prefixed (`svg:a`, `xlink:href`). - Process execution, authorization, cleanup, or containment impact: none. Destination policy and URL sanitization are unchanged. The new boundary test launches a local headless Chrome only when an executable is already installed. - Generated metadata (`docs/public/product-catalog.json`), package, or installation impact (CI generates and validates ignored catalogs and `skills/`; edit authored instructions in `skill-src/` and never commit build outputs): none. No schema, catalog, package, or skill source change. ## Evidence and regression coverage Executed against `upstream/main` (`3749ccec`) with the new tests applied and the production change reverted, then again with the change. - Tests added or updated: `src/browser/CdpCaptureDocuments.baseUrl.test.ts` (prefixed `svg:a` and `svg:image`, orphan `svg:a`, `svg:a` under `svg:foreignObject`, XHTML `<a>` and `<h:a>` excluded, custom XLink prefix, literal `xlink` rebound to another namespace, href wins including empty href, outside-policy origin, `bogus:svg`, HTML document unchanged, xmlns on the element itself, forward ancestor, 4000 nested elements under 1000ms). `tests/boundary/browser/cdpSvgXlinkHref.test.ts` (real headless Chrome, page served as `application/xhtml+xml`). - Base reproduction or other evidence: on `3749ccec`, `npm run test:focused -- src/browser/CdpCaptureDocuments.baseUrl.test.ts tests/boundary/browser/cdpSvgXlinkHref.test.ts` failed 9 tests and passed 36. The XHTML Chrome test received 2 `href` entries (the plain `href` and the empty `href`) and not the 8 entries the page should produce. Prefixed snapshot fixtures produced `[]` instead of the SVG URL. The custom-prefix fixture reported `https://app.example.test/not-xlink` from the literal name `xlink:href` instead of `https://app.example.test/xl-custom`. The HTML-document test and the plain-href precedence test passed on that base because plain `href` is already collected for every element and HTML names are unchanged. - User-visible CLI/MCP output (if applicable): the headless Chrome boundary test reads `metadata.dom_urls` from `CdpBrowserProvider.inspectPage`, the same records the CLI and MCP adapters return. For `<svg:a xlink:href="/xl-pa1">` the entry is attribute `href`, url `{origin}/xl-pa1`, destination_scope `approved`. The DOM node stays `svg:a` with attribute name `xlink:href`. The CLI and MCP routes were not invoked. - Remaining proof gaps: Firefox, Safari, and Chromium builds other than the installed Google Chrome were not run. The full `verify:browser` lane, `inspect-web-page` CLI, and MCP route were not run. No real-world SVG page beyond the fixture. For evidence-bearing changes: - [x] Observed, derived, and inferred claims remain distinguishable. - [x] Artifact identity, source provenance, and failed attempts remain preserved. - [x] Unsupported, incomplete, unavailable, or uncertain outcomes remain visible. ## Validation performed - `npm ci` (Node v24.18.0, npm 11.16.0) - passed. - `npm run format:check` on unmodified `upstream/main` `3749ccec` - passed (`All matched files use the correct format`, 2491 files). The earlier unrelated format failure is not present on this base. - `npm run test:focused -- src/browser/CdpCaptureDocuments.baseUrl.test.ts tests/boundary/browser/cdpSvgXlinkHref.test.ts` on that base with these tests - failed, 9 failed / 36 passed. `REA_BROWSER_NO_SANDBOX=true`. - Same focused command on this branch - passed, 45 tests. Unit file about 25ms, including the 4000-element case. Boundary file 2 tests, about 443ms. Google Chrome 148.0.7778.96. - `npm run check` - passed. Turbo typecheck, lint, format:check, knip, and metadata:check succeeded. Lint reported 707 warnings and 0 errors in unrelated files. Architecture guards passed across 1110 source files. `format:check` passed on this branch as well (`All matched files use the correct format`, 2492 files). - `npm run verify:test-discovery` - passed (`Discovered all 811 test files exactly once`). - Pre-commit lint-staged (oxfmt, oxlint) and pre-push `npm run check:fast` - passed on push. ## Compatibility, safety, and release - Breaking changes or migration steps: none. HTML `dom_urls` output is unchanged. XHTML pages gain `href` entries that were previously missing. Callers that assumed those links were absent will now see them, with the same attribute enum and destination policy. - Real Hopper/Ghidra, browser, or OS coverage: headless Google Chrome 148.0.7778.96 on Linux, sandbox disabled via `REA_BROWSER_NO_SANDBOX=true` because this environment requires it. The test skips when no Chrome-family executable is installed. Hopper and Ghidra are not involved. - Package or release metadata impact: none. No version bump, changelog edit, or generated catalog commit. Release Please will pick up the conventional commit subject. - Security, privacy, process, or containment review: destination policy, URL sanitization, and allowed origins are unchanged. Outside-policy and non-http XLink values stay `outside_policy` or `unsupported`. No new process authority. The boundary test starts a local Chrome and a loopback fixture server and tears both down. ## Review checklist - [x] This PR addresses a concrete problem or an agreed enhancement. - [x] The PR has one focused outcome and the title follows `type(scope): outcome`. - [x] Related issue is linked, or the reason for not linking one is stated above. - [x] Tests cover changed observable behavior and meaningful failure paths. - [x] Owning docs, contracts, and generated metadata are updated where needed. - [x] User-visible CLI/MCP changes include representative output. - [x] I checked the final diff for secrets, unrelated cleanup, and unsupported claims. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-31 00:54:41 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#4752
No description provided.