Skip to content

feat(rendering): establish canonical document pipeline - #19

Merged
andycamara merged 3 commits into
mainfrom
feature/render-v2-canonical-document
Aug 3, 2026
Merged

andycamara merged 3 commits into
mainfrom
feature/render-v2-canonical-document

Conversation

@andycamara

@andycamara andycamara commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • establishes RenderDocumentV1 with canonical node, attribute, content, runtime, CSP, serialization, and hashing contracts
  • compiles header, page, and footer as one resolved canonical document for public HTML, snapshots, thumbnails, and storage-backed rendering
  • preserves ordered and description-list semantics through the shared resolver
  • ports the accepted editor lifecycle and canonical persistence handoff corrections from aria-demo
  • retains PARITY-STAGE-UTILITY-PASTE as the named Phase 6 expected failure

Validation

  • exact tree parity with the accepted aria-demo checkpoint: 64c0c6a6322d2826456a2ff398cabdce2410c102
  • full Vitest: 862 files passed, 1 skipped; 5,015 tests passed, 1 skipped
  • rendering foundation: Node 41 passed plus 5 expected failures; workerd 40 passed
  • runtime stability: 3 passed
  • browser canvas parity: 18 passed
  • project check: 0 errors
  • Node and Cloudflare production builds passed
  • bundle, Cloudflare free-limit, rendering boundary, writer, route-isolation, runtime smoke, package smoke, and git diff checks passed

No release, version bump, migration, bulk rewrite, or Phase 6 implementation is included.

Summary by CodeRabbit

  • New Features
    • Added reliable cache delivery tracking and retry status for publishing and unpublishing.
    • Added Scheduled and Modified page filters.
    • Added unordered, ordered, and description-list editing with semantic HTML output.
    • Improved drag-and-drop insertion for grids, wrapping layouts, reversed directions, and iframe scrolling.
    • Introduced consistent canonical page rendering for improved output parity.
  • Bug Fixes
    • Improved save-and-publish reliability by completing pending edits, validating revisions, and reporting failures.
    • Improved modified-page detection and cache invalidation handling.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Publishing now persists and delivers cache invalidation jobs. Editor saves and publishes now coordinate asynchronous commits and version checks. List semantics support description lists. Rendering uses canonical documents. Stage drag-and-drop supports wrapped, grid, and cancellable interactions. Studio adds scheduled and modified page filters.

Changes

Publishing and cache invalidation

Layer / File(s) Summary
Invalidation jobs and delivery
aria/lib/cache/*, aria/lib/localization/invalidationDrain.ts, aria/lib/cloudflare/*
Adds validated invalidation jobs, Workers Cache API purging, retries, job filtering, and delivery results.
Publication persistence and actions
aria/actions/publishing.ts, aria/lib/storage/..., aria/worker/*
Publish and unpublish persist jobs atomically, attempt delivery, validate versions, and return delivery metadata.
Publishing validation
aria/tests/cache/*, aria/tests/integration/*, aria/tests/storage/*, aria/tests/publishing/*
Tests cover retries, scheduled publication, conflict fences, atomic persistence, and purge behavior.

Editor persistence and coordination

Layer / File(s) Summary
Commit coordination
aria/admin/features/Core/composables/editorCommitCoordinator.ts, usePropertySave.ts, useStylePreviewQueue.ts
Tracks editor commits, flushes pending work, propagates failures, and blocks mutations during publish.
Save and publish flow
aria/admin/features/Core/composables/useSavePublish.ts, aria/actions/save.ts
Adds structured save outcomes, fingerprints, revision checks, stylesheet regeneration, delivery polling, and prepared surface persistence.

Semantic lists

Layer / File(s) Summary
List conversion and controls
aria/lib/blocks/listNodes.ts, aria/admin/features/Inspector/*, aria/admin/lib/icons.ts, aria/admin/i18n/messages/inspector/list.ts
Adds unordered, ordered, and description-list modes with history-based conversion and description groups.
Native rendering and tests
aria/admin/features/Stage/components/StageFrame.vue, aria/lib/rendering/canonical/renderDocumentHtml.ts, aria/tests/lib/*list*, aria/tests/rendering-parity/*
Renders native list tags and validates conversion, paste parity, and mounted semantics.

Canonical rendering

Layer / File(s) Summary
Canonical contracts and compiler
aria/lib/rendering/canonical/contract.ts, compileCanonicalNode.ts, compileRenderDocument.ts
Defines versioned render-document contracts and compiles resolved surfaces into canonical nodes and documents.
HTML rendering and integration
aria/lib/rendering/canonical/renderDocumentHtml.ts, renderPageDslToHtml.ts, renderPageHtml.ts, aria/lib/blocks/nodesToHtml.ts
Centralizes HTML, stylesheet, layout, runtime-manifest, CSP, and document-hash generation.
Rendering parity
aria/tests/rendering-foundation/*, vitest.rendering-foundation.*
Validates deterministic serialization, hashes, metadata, resources, semantic HTML, and frozen documents.

Stage and Studio updates

Layer / File(s) Summary
Drag-and-drop geometry and lifecycle
aria/admin/features/Stage/dragdrop/*, CanvasOverlayLayer.vue, aria/tests/features/stage/*
Supports wrapped flex and grid insertion, animation-frame feedback, iframe auto-scroll, and explicit drag cancellation.
Studio page filters
aria/admin/features/Studio/*, aria/admin/i18n/messages/domains/sidebar.ts, aria/tests/features/studio/*, aria/tests/views/*
Adds scheduled and modified filters, route parsing, reactive synchronization, and localized labels.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: establishing the canonical rendering document pipeline.
Description check ✅ Passed The description clearly explains the change and provides detailed validation results, although several template sections are omitted.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/render-v2-canonical-document

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 15

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
aria/actions/publishing.ts (1)

309-327: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Keep the job identifier collision-resistant.

aria_cache_invalidation_jobs.id is the unique primary key, but createPagePublicationInvalidationJob only stores idempotencyKey.slice(0, 160). Two distinct page publication keys with long page IDs or versions can truncate to the same id. The INSERT ... ON CONFLICT(idempotency_key) DO NOTHING guard then does not help here, and the same insert is batched with the page publish/unpublish statements. Derive id from the full idempotencyKey, use a stable hash, or make the primary key safe against truncated prefixes.

🤖 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 `@aria/actions/publishing.ts` around lines 309 - 327, The
createPagePublicationInvalidationJob implementation must ensure
aria_cache_invalidation_jobs.id remains collision-resistant instead of relying
on idempotencyKey.slice(0, 160). Derive the primary-key identifier from the full
idempotencyKey using a stable collision-resistant representation, while
preserving idempotencyKey for the existing conflict guard and keeping
publication invalidation behavior unchanged.
🟡 Minor comments (6)
aria/tests/rendering-foundation/canonical-document.parity.test.ts-162-162 (1)

162-162: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The ordering assertion passes when "Header" is absent.

String.prototype.indexOf returns -1 for a missing substring. -1 is less than any valid index, so Line 162 passes even when the header region contributes no output. Assert presence before you compare positions.

💚 Proposed fix
+    expect(html).toContain("Header");
+    expect(html).toContain("Footer");
     expect(html.indexOf("Header")).toBeLessThan(html.indexOf("<dl>"));
     expect(html.indexOf("<dl>")).toBeLessThan(html.indexOf("Footer"));
🤖 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 `@aria/tests/rendering-foundation/canonical-document.parity.test.ts` at line
162, Update the ordering assertion around the HTML checks to first assert that
the expected "Header" content is present, then compare its index with the <dl>
index. Ensure the test fails when the header is absent while preserving the
existing ordering requirement.
aria/lib/rendering/canonical/compileCanonicalNode.ts-181-181 (1)

181-181: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fragment detection does not match the case handling used elsewhere in this file.

Line 94 and Line 122 normalize node.type with toLowerCase(). Line 181 compares the raw value. A node whose type is "fragment" is not flattened. compileCanonicalNode then emits a <fragment> element or returns null.

Use the same normalization.

🐛 Proposed fix
-    if (node.type === "Fragment") {
+    if (node.type.toLowerCase() === "fragment") {
       output.push(...compileCanonicalNodes(node.children, options));
       continue;
     }
🤖 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 `@aria/lib/rendering/canonical/compileCanonicalNode.ts` at line 181, Update the
Fragment branch in compileCanonicalNode to normalize node.type with the same
toLowerCase() handling used by the checks near lines 94 and 122, so both
"Fragment" and "fragment" are flattened consistently.
aria/tests/lib/listNodes.test.ts-129-148 (1)

129-148: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

This test locks a dl that contains dt elements and no dd.

ordinaryListItemToDescriptionChild in aria/lib/blocks/listNodes.ts (line 359) defaults every plain list item to the term role. Converting a plain list to description mode therefore produces <dl><dt>…</dt><dt>…</dt></dl>. HTML requires each dt group to be followed by at least one dd, so the result is invalid and screen readers announce terms with no definitions.

Confirm this is the intended authoring step. If it is, consider emitting a paired empty dd for each converted item so the document stays valid before the author fills it in.

🤖 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 `@aria/tests/lib/listNodes.test.ts` around lines 129 - 148, Update
ordinaryListItemToDescriptionChild in convertListSemanticMode so each plain list
item converted to description mode emits its term together with a paired empty
description child, producing valid dt/dd groups. Preserve the existing item
content and description-list semantics while updating the test expectations to
account for the added dd nodes.
aria/admin/features/Inspector/composables/useListSemanticMutation.ts-26-30 (1)

26-30: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Undo restores a normalized node, not the original node.

convertListSemanticMode does more than set the element. It also runs normalizeOrdinaryDescriptionParts and injects styles.listStyleType.base when the current base value is not a string. So previousNode can differ from listNode even when the mode is unchanged. After undo, the list can carry a listStyleType that the author never set.

Capture a plain clone of the current node for the undo path.

🐛 Proposed fix
-    const previousNode = convertListSemanticMode(
-      listNode,
-      resolveListSemanticMode(listNode),
-    );
+    const previousNode = structuredClone(listNode);
     const nextNode = convertListSemanticMode(listNode, nextMode);
🤖 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 `@aria/admin/features/Inspector/composables/useListSemanticMutation.ts` around
lines 26 - 30, Update the undo snapshot in the list semantic mutation flow to
capture a plain clone of the current listNode before calling
convertListSemanticMode, rather than deriving previousNode through conversion.
Keep convertListSemanticMode for creating nextNode, and ensure undo restores the
cloned original node without normalization or injected listStyleType values.
aria/admin/features/Stage/dragdrop/useCanvasDrop.ts-361-383 (1)

361-383: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Stop the auto-scroll loop when the iframe cannot scroll further.

autoScrollVelocity depends on pointer position only. If the iframe already sits at a scroll limit, the velocity stays non-zero and step keeps rescheduling. Each iteration calls scheduleDropFeedback, which re-runs resolveLibraryDropIntent with elementsFromPoint hit testing and repeated style and rectangle reads. The pointer only needs to rest inside the 56px edge band for this per-frame work to continue for the whole drag.

Compare the scroll offset before and after scrollBy. If it did not change on both axes, stop the loop.

🔧 Proposed guard
     const step = () => {
       if (!state.value.isDragging) {
         autoScrollFrame = null;
         return;
       }
       const x = autoScrollVelocity(point.frameX, iframeWindow.innerWidth);
       const y = autoScrollVelocity(point.frameY, iframeWindow.innerHeight);
       if (x === 0 && y === 0) {
         autoScrollFrame = null;
         return;
       }
+      const beforeX = iframeWindow.scrollX;
+      const beforeY = iframeWindow.scrollY;
       iframeWindow.scrollBy(x, y);
+      if (iframeWindow.scrollX === beforeX && iframeWindow.scrollY === beforeY) {
+        autoScrollFrame = null;
+        return;
+      }
       scheduleDropFeedback(point);
       autoScrollFrame = window.requestAnimationFrame(step);
     };
🤖 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 `@aria/admin/features/Stage/dragdrop/useCanvasDrop.ts` around lines 361 - 383,
Update the step function inside updateAutoScroll to capture the iframeWindow
scroll offsets before calling scrollBy, then compare them afterward; when
neither axis changes, clear autoScrollFrame and return without scheduling drop
feedback or another animation frame. Preserve the existing velocity-zero and
drag-state termination paths.
aria/tests/cache/public-page-cache.test.ts-28-34 (1)

28-34: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reset purgeMock before each test.

purgeMock is a standalone vi.fn(), and the Vitest config does not enable clearMocks or restoreMocks. Add purgeMock.mockReset() to beforeEach so tests after the mock call retain their expected mock state.

💚 Proposed fix
 describe("public page cache", () => {
   beforeEach(() => {
+    purgeMock.mockReset();
     purgeMock.mockResolvedValue({ success: true, errors: [] });
   });
🤖 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 `@aria/tests/cache/public-page-cache.test.ts` around lines 28 - 34, Reset the
standalone purgeMock state in the beforeEach hook by calling
purgeMock.mockReset() before configuring its resolved value. Keep the existing
mockResolvedValue setup and afterEach restoration unchanged.
🧹 Nitpick comments (23)
aria/tests/rendering-foundation/canonical-document.parity.test.ts (2)

187-189: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document how to regenerate the pinned documentHash.

Line 188 pins a SHA-256 over every field of the serialized document. Any change to the contract, the renderer output, or the generated CSS breaks this assertion, and the failure message reports only two hex strings. Line 186 and Line 196 already assert determinism and self-consistency.

Add a comment that states the regeneration command, or move the literal to a named constant with that note. Keep the assertion if cross-runtime byte parity is a required guarantee.

🤖 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 `@aria/tests/rendering-foundation/canonical-document.parity.test.ts` around
lines 187 - 189, Document how to regenerate the pinned documentHash near the
assertion in the canonical document parity test, including the exact
regeneration command or a named constant with that note. Preserve the existing
hash assertion and the determinism and self-consistency checks.

196-199: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add negative coverage for hash validation and the freeze option.

The suite asserts only the passing direction. Add cases that confirm hasValidRenderDocumentHash returns false for a document with a modified field, and that compileRenderDocument with freeze: false returns a mutable document. Both paths are new public behavior in this PR.

Do you want me to generate these test cases?

🤖 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 `@aria/tests/rendering-foundation/canonical-document.parity.test.ts` around
lines 196 - 199, Add negative test coverage in the canonical render document
parity suite: modify a field on the document and assert
hasValidRenderDocumentHash returns false, and compile a document with freeze:
false then assert the returned document is mutable. Keep the existing valid-hash
and frozen-document assertions unchanged.
aria/lib/rendering/canonical/compileCanonicalNode.ts (2)

136-136: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

namespace is not inherited by SVG descendants.

Line 136 assigns "svg" only when tagName equals "svg". A compiled child such as path, g, or use receives "html". Thread the parent namespace through CompileCanonicalNodeOptions so descendants of an svg element keep the svg namespace.

🤖 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 `@aria/lib/rendering/canonical/compileCanonicalNode.ts` at line 136, Update
CompileCanonicalNodeOptions and the compileCanonicalNode flow so the parent
namespace is passed into child compilation and inherited by descendants. Ensure
SVG descendants such as path, g, and use retain the "svg" namespace instead of
deriving "html" solely from their own tagName, while preserving the root svg
namespace assignment.

141-167: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Hoist the tag lists and reuse the computed class tokens.

Line 141 and Line 158 allocate a new array for every node. Move both to module-level Set constants. Line 163 also rescans opening.attributes for class and re-splits the value, although Line 139 already produced classTokens. Compute the tokens once and test them for aria-managed-image.

🤖 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 `@aria/lib/rendering/canonical/compileCanonicalNode.ts` around lines 141 - 167,
Update compileCanonicalNode’s tag checks by hoisting the void-element and
interactive tag lists into module-level Set constants, then reuse those sets for
membership tests instead of allocating arrays per node. In the managedImage
capability calculation, reuse the existing classTokens computed near line 139 to
detect aria-managed-image rather than rescanning opening.attributes and
splitting the class value again.
aria/lib/rendering/canonical/compileCanonicalAttributes.ts (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Two modules only re-export symbols that renderDocumentHtml.ts implements. The canonical compiler API is declared across separate files while one file holds every implementation. Consumers gain two import paths per symbol, and ownership of the compiler surface becomes unclear. compileCanonicalNode.ts Line 21 repeats the pattern for compileCanonicalNodeHtml and compileCanonicalTag.

  • aria/lib/rendering/canonical/compileCanonicalAttributes.ts#L1-L1: move the compileCanonicalAttributes implementation into this file, or delete the file and import from renderDocumentHtml.ts.
  • aria/lib/rendering/canonical/compileCanonicalContent.ts#L1-L1: apply the same decision to compileCanonicalContent so both modules follow one convention.
🤖 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 `@aria/lib/rendering/canonical/compileCanonicalAttributes.ts` at line 1,
Consolidate the canonical compiler exports by removing the redundant re-export
modules and importing compileCanonicalAttributes and compileCanonicalContent
directly from renderDocumentHtml.ts; apply this consistently at
aria/lib/rendering/canonical/compileCanonicalAttributes.ts:1-1 and
aria/lib/rendering/canonical/compileCanonicalContent.ts:1-1, updating any
consumers as needed.
aria/lib/rendering/canonical/compileRenderDocument.ts (1)

67-113: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the icon-resource fallback once.

options.iconResources ?? options.document.iconResources is repeated at Lines 70, 76, and 111. A single local keeps the three freshIconResources copies aligned if the fallback rule changes.

♻️ Proposed refactor
+  const baseIconResources =
+    options.iconResources ?? options.document.iconResources;
   const documentOptions: NodeToHtmlDocumentOptions = {
     ...options.document,
-    iconResources: freshIconResources(
-      options.iconResources ?? options.document.iconResources,
-    ),
+    iconResources: freshIconResources(baseIconResources),
   };
   const styleMode = resolvePublishedHtmlRenderStyleMode(documentOptions);
   const html = nodesToHtmlDocument(roots, documentOptions);
-  const regionIconResources = freshIconResources(
-    options.iconResources ?? options.document.iconResources,
-  );
+  const regionIconResources = freshIconResources(baseIconResources);
     roots: compileCanonicalNodes(roots, {
       breakpoints: documentOptions.breakpoints,
       styleMode,
-      iconResources: freshIconResources(
-        options.iconResources ?? options.document.iconResources,
-      ),
+      iconResources: freshIconResources(baseIconResources),
     }),
🤖 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 `@aria/lib/rendering/canonical/compileRenderDocument.ts` around lines 67 - 113,
Extract options.iconResources ?? options.document.iconResources into a local
fallback value near the start of the render flow, then reuse it for the
documentOptions, regionIconResources, and compileCanonicalNodes
freshIconResources calls. Remove the repeated fallback expressions while
preserving the existing resource-refresh behavior.
aria/tests/core/useSavePublish.test.ts (1)

404-430: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that pending delivery triggers polling.

The test proves the success toast and the absence of an error toast. It does not assert that deliveryStatusMock was called with the returned deliveryJobId. Add that assertion so the test protects the polling contract in pollPublishDelivery.

💚 Proposed assertion
     expect(toastSuccessMock).toHaveBeenCalledWith("Page Published");
     expect(toastErrorMock).not.toHaveBeenCalled();
+    expect(deliveryStatusMock).toHaveBeenCalledWith({
+      jobId: "public-route:publish:page-home:v2",
+    });
🤖 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 `@aria/tests/core/useSavePublish.test.ts` around lines 404 - 430, Update the
test around handleSaveAndPublish to assert that pending delivery invokes
deliveryStatusMock with the returned deliveryJobId
"public-route:publish:page-home:v2". Keep the existing toast assertions and
verify the polling contract implemented by pollPublishDelivery.
aria/admin/features/Inspector/composables/useStylePreviewQueue.ts (1)

27-27: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Registration is global; unregistration depends on a component instance.

registerEditorCommitFlusher adds flush to a module-level set. The cleanup runs only inside onBeforeUnmount. If any caller invokes useStylePreviewQueue outside a component setup scope, Vue skips the hook and the flusher stays registered for the process lifetime. Every later settleEditorCommitBarrier call then invokes a stale flush and can call options.applyPreview for a destroyed target.

Guard the registration with getCurrentInstance(), or return the unregister function so non-component callers can release it.

Also applies to: 69-72

🤖 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 `@aria/admin/features/Inspector/composables/useStylePreviewQueue.ts` at line
27, Update useStylePreviewQueue around registerEditorCommitFlusher and its
onBeforeUnmount cleanup so registration only occurs when getCurrentInstance() is
available, or otherwise expose and require the unregisterEditorCommitFlusher
callback for non-component callers. Ensure every registered flush is removable
and cannot remain active after its target is destroyed.
aria/tests/storage/surface-normalization.test.ts (1)

38-56: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add a test that pins the trusted hash to the canonical hash.

This test uses an arbitrary 64-character value, so it only proves pass-through. It does not prove that a marked content_hash equals normalizeSurfaceForPersistence(kind, dsl).sourceHash. That equality is the property savePageDSL relies on for conflict reconciliation and for skipIfContentUnchanged.

Add a case that computes the canonical hash for page, stores it in content_hash with the marker, and asserts the resolved value equals that canonical hash.

🤖 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 `@aria/tests/storage/surface-normalization.test.ts` around lines 38 - 56, Add a
test alongside the existing resolveStoredSemanticSourceHash case that derives
the expected hash via normalizeSurfaceForPersistence("page", dsl).sourceHash,
stores that value as content_hash with the renderSourceHashVersion marker, and
asserts the resolver returns the canonical hash without invoking fallback. Use
valid DSL input so the assertion verifies canonicalization rather than arbitrary
hash pass-through.
aria/admin/features/Core/composables/useSavePublish.ts (1)

201-213: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Delivery polling has no delay between attempts.

The loop issues the second deliveryStatus request immediately after the first. A job that needs a few hundred milliseconds is almost always still pending, so the second attempt adds a request without improving the result. Add a short delay with backoff between attempts.

🤖 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 `@aria/admin/features/Core/composables/useSavePublish.ts` around lines 201 -
213, Update pollPublishDelivery to wait between deliveryStatus attempts,
applying a short increasing backoff before each retry while preserving the
existing two-attempt limit and ready/pending outcomes.
aria/tests/rendering-parity/p0CanvasFixture.ts (1)

142-149: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Clarify the contradictory ordered: true on the dl fixture node.

The node sets element: "dl" and ordered: true. resolveListSemanticMode returns description because element wins, so the flag has no effect today. Either remove the flag, or add a short comment stating that it intentionally guards against tag resolution that prefers ordered over element.

🤖 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 `@aria/tests/rendering-parity/p0CanvasFixture.ts` around lines 142 - 149,
Clarify the intentional `ordered: true` setting on the `parity-description-list`
fixture in the `parity-description-list` node: either remove the unused flag or
add a brief comment explaining that it guards against future tag resolution
preferring `ordered` over `element`.
aria/admin/features/Inspector/inputs/ListProperty.vue (1)

249-258: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Derive isDescriptionList from listSemanticMode.

listSemanticMode already resolves description semantics through resolveListSemanticMode. The separate props.element === "dl" check duplicates that rule in the component. If the resolver changes, the two checks can diverge.

♻️ Proposed refactor
-const isDescriptionList = computed(
-  () => targetNode.value?.props?.element === "dl",
-);
+const isDescriptionList = computed(
+  () => listSemanticMode.value === "description",
+);
🤖 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 `@aria/admin/features/Inspector/inputs/ListProperty.vue` around lines 249 -
258, Update the isDescriptionList computed property to derive its value from the
existing listSemanticMode computed state, which resolves semantics through
resolveListSemanticMode, instead of reading targetNode.value?.props?.element
directly. Leave showMarkerControls and the other computed properties unchanged.
aria/tests/rendering-parity/canvas-parity.spec.ts (1)

442-442: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Also assert list semantics on the public frame.

expectMountedListSemantics runs against stageFrame only. openSurface returns a Frame for both surfaces, so the same helper can validate the published HTML. Public-side coverage is what catches Stage-versus-canonical divergence, such as the dl handling difference in aria/lib/rendering/canonical/renderDocumentHtml.ts.

🤖 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 `@aria/tests/rendering-parity/canvas-parity.spec.ts` at line 442, Extend the
parity test after the existing expectMountedListSemantics(stageFrame) call to
run the same helper against the public Frame returned by openSurface. Keep the
stageFrame assertion and validate both surfaces so canonical HTML list semantics
are covered.
aria/admin/features/Inspector/composables/useListSemanticMutation.ts (1)

22-24: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Localize the operator-facing error strings.

"The editor is saving" and "Failed to change list type" are hardcoded English. ListProperty.vue assigns result.error directly to validationError, and it uses t("inspector.validation.invalidList") only when result.error is undefined. So these strings reach the inspector untranslated. Return message keys, or resolve the text in the component through useStudioI18n.

Also applies to: 45-50

🤖 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 `@aria/admin/features/Inspector/composables/useListSemanticMutation.ts` around
lines 22 - 24, Update the error handling in useListSemanticMutation, including
the isEditorMutationLocked branch and the failed list-type mutation path, to
return localization message keys instead of hardcoded English strings. Ensure
ListProperty.vue resolves result.error through useStudioI18n before assigning
validationError, while preserving the existing fallback to
inspector.validation.invalidList when no error is provided.
aria/tests/inputs/ListProperty.test.ts (1)

608-637: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract one shared Tabs stub factory.

The two stub blocks are near-duplicates. The second block omits modelValue from the provided context, so the two tests exercise slightly different stub behavior. A shared factory keeps them aligned.

Also applies to: 700-728

🤖 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 `@aria/tests/inputs/ListProperty.test.ts` around lines 608 - 637, Extract the
duplicated Tabs, TabsList, and TabsTrigger definitions into one shared stub
factory used by both test blocks around the existing definitions and the
corresponding block near the second location. Ensure the factory consistently
provides both the reactive modelValue and select through tabsSelectionKey, while
preserving the existing props, emits, rendering, and click behavior.
aria/admin/features/Stage/dragdrop/canvasDropInsertion.ts (3)

259-328: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Cache child rectangles in the grid index pass.

The function reads getBoundingClientRect for each child during row grouping, row sorting, row-bound computation, and the visual scan. It also calls children.indexOf(child), which is O(n²) for the row. Each rectangle read can force layout, and drag feedback runs this function per animation frame.

Measure each child once into an array of { element, index, rect } entries, then group, sort, and scan over that array. The insertion-index results stay identical.

🤖 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 `@aria/admin/features/Stage/dragdrop/canvasDropInsertion.ts` around lines 259 -
328, Update calculateGridInsertionIndex to measure each child once into cached
entries containing its element, original index, and DOMRect, then use those
entries for row grouping, sorting, bounds calculation, and visual scanning.
Replace children.indexOf lookups with the cached index while preserving the
existing insertion-index behavior and ordering.

387-401: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Share the row tolerance and simplify the direction test.

sameVisualRow repeats the 8px tolerance that calculateGridInsertionIndex defines as rowTolerance. Extract one module constant so both row checks stay aligned. !(a !== b) is equivalent to a === b; the direct comparison reads clearer.

♻️ Proposed cleanup
+const ROW_TOLERANCE_PX = 8;
+
 function sameVisualRow(a: ViewportRect, b: ViewportRect): boolean {
-  return Math.abs(a.top - b.top) <= 8;
+  return Math.abs(a.top - b.top) <= ROW_TOLERANCE_PX;
 }
@@
-  const domAscendsVisually = !(reversedByFlex !== reversedByDirection);
+  const domAscendsVisually = reversedByFlex === reversedByDirection;
🤖 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 `@aria/admin/features/Stage/dragdrop/canvasDropInsertion.ts` around lines 387 -
401, Extract the 8px row tolerance into a shared module-level constant and use
it in both sameVisualRow and calculateGridInsertionIndex so their thresholds
remain aligned. In computeHorizontalFlowPlaceholder, simplify the
domAscendsVisually direction logic by directly comparing reversedByFlex and
reversedByDirection with equality.

217-257: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Resolve the drop-zone style once per layout pass.

readDropStyle calls getComputedStyle on each invocation. One computeAddElementsDropLayout call reaches it through isGridDropZone, isWrappedHorizontalDropZone, readDropAxis, and computeHorizontalFlowPlaceholder, so the same element is measured six or more times. useCanvasDrop.updateAutoScroll repeats this path on every animation frame while the pointer stays in the edge zone, so each forced style read lands on the drag hot path.

Resolve DropZoneStyle once in computeAddElementsDropLayout and pass it to calculateInsertionIndexMidline, computePlaceholderViewportRect, and computeHorizontalFlowPlaceholder. Keep the current defaults so behavior does not change.

🤖 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 `@aria/admin/features/Stage/dragdrop/canvasDropInsertion.ts` around lines 217 -
257, Update computeAddElementsDropLayout to call readDropStyle once per layout
pass and reuse the resulting DropZoneStyle throughout the calculation. Pass the
cached style into calculateInsertionIndexMidline,
computePlaceholderViewportRect, and computeHorizontalFlowPlaceholder, adjusting
their related helpers such as readDropAxis, isGridDropZone, and
isWrappedHorizontalDropZone to consume it without additional computed-style
reads. Preserve all existing fallback defaults and behavior.
aria/tests/features/stage/canvasDropInsertion.test.ts (1)

26-51: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Isolate the defaultView stub per element.

parent.ownerDocument is the shared global document, so each mockFlexParent call replaces document.defaultView for every element in the file. The last call wins. The test at lines 269-299 therefore depends on statement order: it must compute the row layout before it creates the column parent, otherwise the row reads column styles. No hook restores the real defaultView, so the stub also leaks into later tests.

Return the style descriptor per element and stub getComputedStyle so it dispatches on the element it receives. Restore document.defaultView in an afterEach hook.

🤖 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 `@aria/tests/features/stage/canvasDropInsertion.test.ts` around lines 26 - 51,
Update mockFlexParent so style configuration is associated with each created
parent and document.defaultView.getComputedStyle dispatches based on the element
argument, rather than replacing shared defaultView per call. Preserve the
existing display, flex, direction, and grid style values for each mock element,
and add an afterEach cleanup that restores the original document.defaultView
after every test.
aria/tests/features/stage/useCanvasDrop.test.ts (1)

79-108: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Select the feedback frame explicitly and remove test listeners.

[...animationFrames.values()][0] relies on the composable scheduling the feedback frame before the auto-scroll frame. handleDragOver calls scheduleDropFeedback and then updateAutoScroll, so the order holds today, but a reorder in production code would make this test drain the auto-scroll callback instead and fail for the wrong reason. Run every queued callback, or record the callback identity you expect.

The canvas:add-elements-insertion and canvas:drop listeners are never removed. They stay registered for the second test. Register them with an AbortController signal and abort in afterEach.

🤖 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 `@aria/tests/features/stage/useCanvasDrop.test.ts` around lines 79 - 108, Make
the useCanvasDrop test select the expected feedback animation callback without
depending on Map insertion order, by recording or otherwise identifying the
callback scheduled by scheduleDropFeedback and invoking that callback
explicitly. Also register the canvas:add-elements-insertion and canvas:drop
listeners with an AbortController signal, then abort it in afterEach so
listeners do not leak into the second test.
aria/lib/cloudflare/workers-shim.ts (1)

37-47: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Align the shim signature with the declared module type.

aria/lib/types/cloudflare-workers-module.d.ts declares purge(options: CachePurgeOptions), and CachePurgeResult is declared in both files. The shim purge() accepts no parameter. Accept and ignore the options argument so the shim matches the contract, and reuse one result type to avoid drift.

♻️ Proposed alignment
-export interface CachePurgeResult {
-  success: boolean;
-  errors: Array<{ code: number; message: string }>;
-}
+import type {
+  CachePurgeOptions,
+  CachePurgeResult,
+} from "cloudflare:workers";
+
+export type { CachePurgeResult };
 
 /** Node has no Workers edge cache; a purge is therefore already satisfied. */
 export const cache = {
-  async purge(): Promise<CachePurgeResult> {
+  async purge(_options: CachePurgeOptions): Promise<CachePurgeResult> {
     return { success: true, errors: [] };
   },
 };

If the shim itself provides the cloudflare:workers module at build time, declare the types locally instead of importing them.

🤖 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 `@aria/lib/cloudflare/workers-shim.ts` around lines 37 - 47, Update the
cache.purge method in workers-shim.ts to accept and ignore the required
CachePurgeOptions argument, matching the cloudflare-workers-module declaration.
Consolidate CachePurgeResult so the shim reuses the existing declared type
rather than defining a duplicate; if the shim supplies the module itself, keep
the necessary type declarations local instead of importing them.
aria/tests/cache/public-page-cache.test.ts (1)

9-14: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the stale Cloudflare credential mock.

The purge path no longer reads API tokens or zone identifiers. This mock now only injects unused fake credentials. Delete it, or keep it only if another import in this file still calls getCloudflareEnv.

🤖 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 `@aria/tests/cache/public-page-cache.test.ts` around lines 9 - 14, Remove the
vi.mock for getCloudflareEnv from the cache test, since the purge path no longer
consumes Cloudflare credentials. Retain the mock only if another import in this
file still invokes getCloudflareEnv.
aria/tests/integration/publishing-snapshot-injection.test.ts (1)

99-104: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider adding a regression test for a concurrent draft save during publish.

The getPageVersionPins mock always returns matching draft/published/current versions, so this suite never exercises the case where draftVersion differs from publishedVersion right after a successful publish (see the reconciliation race raised on aria/actions/publishing.ts lines 340-354). Add a test where mockGetPageVersionPins returns a draftVersion different from the just-published version, to confirm the publish action's behavior in that case.

Also applies to: 266-293

🤖 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 `@aria/tests/integration/publishing-snapshot-injection.test.ts` around lines 99
- 104, Add a regression test in the publishing snapshot injection suite for a
concurrent draft save during publish. Configure mockGetPageVersionPins to return
a draftVersion different from the just-published version while preserving the
published and current versions, then assert the publishing action’s
reconciliation behavior for that mismatch. Cover the existing setup shared by
the referenced mock configuration without changing matching-version scenarios.
🤖 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 `@aria/actions/publishing.ts`:
- Around line 413-428: Update the deliveryStatus handler to stop unconditionally
forcing delivery via the true argument to deliverCommittedInvalidation. Respect
the job’s nextAttemptAt backoff by default, and only bypass it when the API
explicitly accepts and validates an immediate-retry request; otherwise preserve
normal delivery behavior.
- Around line 340-354: The post-publish reconciliation in publishPageDSL must
not fail when draftVersion differs from publishedVersion because of a concurrent
autosave. Remove draftVersion from the fatal condition or make that divergence
non-fatal, while retaining validation that publishedVersion matches the expected
publishedVersion and avoiding conversion of a successful publish into
PUBLISH_FAILED.

In `@aria/admin/features/Core/composables/editorCommitCoordinator.ts`:
- Around line 74-76: Update the module-scoped commit failure handling used by
settleEditorCommitBarrier so failures cannot survive an empty tracked commit set
and abort a later unrelated save. Clear or scope failures when the tracked
commits become empty, and preserve all relevant failures instead of discarding
entries after the first; ensure the barrier only throws failures associated with
commits pending for that barrier.
- Around line 47-52: Bound the loop in awaitTrackedCommits so repeatedly
enqueued follow-up commits cannot keep the barrier running indefinitely. Track
iterations and stop after a finite limit, using the existing error or
continuation behavior appropriate for save and publish so loadingState.isSaving
can resolve instead of remaining stuck.

In `@aria/admin/features/Core/composables/usePropertySave.ts`:
- Around line 1857-1874: Update the commit-tracking flow around saveProperty,
saveProperties, and saveNodeUpdates so benign false results from validation or
missing context do not become commit failures. Adjust trackEditorCommit or these
wrappers to track only successful real commits, while preserving thrown errors
as failures and keeping the existing lock behavior unchanged.

In `@aria/admin/features/Core/composables/useSavePublish.ts`:
- Around line 492-526: Update createDocumentFingerprint to serialize its payload
with a stable, recursively sorted-key serializer instead of direct
JSON.stringify, so authoredDocument and linkedLayoutSlots produce identical
fingerprints regardless of property insertion order. Preserve the existing
fingerprint inputs and values while ensuring nested objects are canonicalized
before serialization.

In `@aria/admin/features/Stage/dragdrop/useCanvasDrop.ts`:
- Around line 477-493: Update handleCanvasExit to verify that the drag has
actually left the canvas/drop-zone before calling endDrag(), rather than relying
only on relatedTarget and viewport coordinates. Track valid drag-enter/leave
depth or use current-target containment, and preserve active drops during normal
intra-zone drag movement.

In `@aria/lib/cache/invalidationJobs.ts`:
- Around line 80-102: Update the page and layout tag construction in the
invalidation job’s else branch to use the job’s stored route/slug, locale, and
version fields rather than treating resourceId as the slug. Preserve locale and
version scoping for localization jobs, and ensure page tags are generated with
the published route while layout tags retain their localization metadata.

In `@aria/lib/localization/invalidationDrain.ts`:
- Around line 63-77: Update drainCacheInvalidations to enforce a bounded timeout
for every input.deliver(job) attempt, including retries, so a hanging cache
purge cannot block the request path; use the existing delivery flow and preserve
retry/error propagation while ensuring each attempt fails fast.

In `@aria/lib/rendering/canonical/compileCanonicalNode.ts`:
- Around line 111-121: Update compileCanonicalNode and compileCanonicalNodes to
avoid calling compileCanonicalNodeHtml for each node’s full subtree solely to
obtain its opening element. Use an opening-tag-only compilation path or
serialize and walk the tree once, while preserving the existing opening parsing
and recursive output behavior.
- Around line 29-48: Update compileCanonicalNode to derive the
CanonicalRenderNode attributes from the original node/prop model using
compileCanonicalAttributes, rather than parsing the opening tag produced by
compileCanonicalNodeHtml. Remove the rendered-HTML attribute reparsing path
while preserving the existing canonical attribute values and hashing behavior.
- Around line 99-101: Update the normalizedType === "svg" branch in
compileCanonicalNode to sanitize content before constructing the trusted-svg
child. Reuse the project’s existing SVG sanitization utility if available,
ensuring script elements, event-handler attributes, and javascript: URLs are
removed while preserving valid SVG markup.

In `@aria/lib/rendering/canonical/compileRenderDocument.ts`:
- Around line 37-46: Replace the regex-based extraction in
aria/lib/rendering/canonical/compileRenderDocument.ts lines 37-46 by returning
and assigning the head nodes and generated stylesheet directly from
nodesToHtmlDocument, preserving the compiled structure. In
aria/lib/rendering/renderPageHtml.ts lines 156-160, expose the compiled
RenderDocumentV1 or its head/body fields from renderPageDslToHtml and construct
headHtml and bodyHtml from those fields instead of matching tags in
rendered.html.

In `@aria/lib/rendering/canonical/renderDocumentHtml.ts`:
- Around line 1147-1161: The canonical list-item generation condition in
aria/lib/rendering/canonical/renderDocumentHtml.ts lines 1147-1161 must also
require the resolved tag not be "dl", preventing li children inside description
lists. Keep the existing dl guard in
aria/admin/features/Stage/components/StageFrame.vue lines 1865-1868 unchanged;
it already matches the corrected canonical behavior.

In `@aria/tests/rendering-parity/p0CanvasFixture.ts`:
- Around line 214-249: Add the new fixture IDs—parity-description-list,
parity-unordered-list, and parity-ordered-list—to P0_CANVAS_PARITY_NODE_IDS so
collectBrowserSurfaceSnapshot includes all three list nodes in P0 parity
collection.

---

Outside diff comments:
In `@aria/actions/publishing.ts`:
- Around line 309-327: The createPagePublicationInvalidationJob implementation
must ensure aria_cache_invalidation_jobs.id remains collision-resistant instead
of relying on idempotencyKey.slice(0, 160). Derive the primary-key identifier
from the full idempotencyKey using a stable collision-resistant representation,
while preserving idempotencyKey for the existing conflict guard and keeping
publication invalidation behavior unchanged.

---

Minor comments:
In `@aria/admin/features/Inspector/composables/useListSemanticMutation.ts`:
- Around line 26-30: Update the undo snapshot in the list semantic mutation flow
to capture a plain clone of the current listNode before calling
convertListSemanticMode, rather than deriving previousNode through conversion.
Keep convertListSemanticMode for creating nextNode, and ensure undo restores the
cloned original node without normalization or injected listStyleType values.

In `@aria/admin/features/Stage/dragdrop/useCanvasDrop.ts`:
- Around line 361-383: Update the step function inside updateAutoScroll to
capture the iframeWindow scroll offsets before calling scrollBy, then compare
them afterward; when neither axis changes, clear autoScrollFrame and return
without scheduling drop feedback or another animation frame. Preserve the
existing velocity-zero and drag-state termination paths.

In `@aria/lib/rendering/canonical/compileCanonicalNode.ts`:
- Line 181: Update the Fragment branch in compileCanonicalNode to normalize
node.type with the same toLowerCase() handling used by the checks near lines 94
and 122, so both "Fragment" and "fragment" are flattened consistently.

In `@aria/tests/cache/public-page-cache.test.ts`:
- Around line 28-34: Reset the standalone purgeMock state in the beforeEach hook
by calling purgeMock.mockReset() before configuring its resolved value. Keep the
existing mockResolvedValue setup and afterEach restoration unchanged.

In `@aria/tests/lib/listNodes.test.ts`:
- Around line 129-148: Update ordinaryListItemToDescriptionChild in
convertListSemanticMode so each plain list item converted to description mode
emits its term together with a paired empty description child, producing valid
dt/dd groups. Preserve the existing item content and description-list semantics
while updating the test expectations to account for the added dd nodes.

In `@aria/tests/rendering-foundation/canonical-document.parity.test.ts`:
- Line 162: Update the ordering assertion around the HTML checks to first assert
that the expected "Header" content is present, then compare its index with the
<dl> index. Ensure the test fails when the header is absent while preserving the
existing ordering requirement.

---

Nitpick comments:
In `@aria/admin/features/Core/composables/useSavePublish.ts`:
- Around line 201-213: Update pollPublishDelivery to wait between deliveryStatus
attempts, applying a short increasing backoff before each retry while preserving
the existing two-attempt limit and ready/pending outcomes.

In `@aria/admin/features/Inspector/composables/useListSemanticMutation.ts`:
- Around line 22-24: Update the error handling in useListSemanticMutation,
including the isEditorMutationLocked branch and the failed list-type mutation
path, to return localization message keys instead of hardcoded English strings.
Ensure ListProperty.vue resolves result.error through useStudioI18n before
assigning validationError, while preserving the existing fallback to
inspector.validation.invalidList when no error is provided.

In `@aria/admin/features/Inspector/composables/useStylePreviewQueue.ts`:
- Line 27: Update useStylePreviewQueue around registerEditorCommitFlusher and
its onBeforeUnmount cleanup so registration only occurs when
getCurrentInstance() is available, or otherwise expose and require the
unregisterEditorCommitFlusher callback for non-component callers. Ensure every
registered flush is removable and cannot remain active after its target is
destroyed.

In `@aria/admin/features/Inspector/inputs/ListProperty.vue`:
- Around line 249-258: Update the isDescriptionList computed property to derive
its value from the existing listSemanticMode computed state, which resolves
semantics through resolveListSemanticMode, instead of reading
targetNode.value?.props?.element directly. Leave showMarkerControls and the
other computed properties unchanged.

In `@aria/admin/features/Stage/dragdrop/canvasDropInsertion.ts`:
- Around line 259-328: Update calculateGridInsertionIndex to measure each child
once into cached entries containing its element, original index, and DOMRect,
then use those entries for row grouping, sorting, bounds calculation, and visual
scanning. Replace children.indexOf lookups with the cached index while
preserving the existing insertion-index behavior and ordering.
- Around line 387-401: Extract the 8px row tolerance into a shared module-level
constant and use it in both sameVisualRow and calculateGridInsertionIndex so
their thresholds remain aligned. In computeHorizontalFlowPlaceholder, simplify
the domAscendsVisually direction logic by directly comparing reversedByFlex and
reversedByDirection with equality.
- Around line 217-257: Update computeAddElementsDropLayout to call readDropStyle
once per layout pass and reuse the resulting DropZoneStyle throughout the
calculation. Pass the cached style into calculateInsertionIndexMidline,
computePlaceholderViewportRect, and computeHorizontalFlowPlaceholder, adjusting
their related helpers such as readDropAxis, isGridDropZone, and
isWrappedHorizontalDropZone to consume it without additional computed-style
reads. Preserve all existing fallback defaults and behavior.

In `@aria/lib/cloudflare/workers-shim.ts`:
- Around line 37-47: Update the cache.purge method in workers-shim.ts to accept
and ignore the required CachePurgeOptions argument, matching the
cloudflare-workers-module declaration. Consolidate CachePurgeResult so the shim
reuses the existing declared type rather than defining a duplicate; if the shim
supplies the module itself, keep the necessary type declarations local instead
of importing them.

In `@aria/lib/rendering/canonical/compileCanonicalAttributes.ts`:
- Line 1: Consolidate the canonical compiler exports by removing the redundant
re-export modules and importing compileCanonicalAttributes and
compileCanonicalContent directly from renderDocumentHtml.ts; apply this
consistently at aria/lib/rendering/canonical/compileCanonicalAttributes.ts:1-1
and aria/lib/rendering/canonical/compileCanonicalContent.ts:1-1, updating any
consumers as needed.

In `@aria/lib/rendering/canonical/compileCanonicalNode.ts`:
- Line 136: Update CompileCanonicalNodeOptions and the compileCanonicalNode flow
so the parent namespace is passed into child compilation and inherited by
descendants. Ensure SVG descendants such as path, g, and use retain the "svg"
namespace instead of deriving "html" solely from their own tagName, while
preserving the root svg namespace assignment.
- Around line 141-167: Update compileCanonicalNode’s tag checks by hoisting the
void-element and interactive tag lists into module-level Set constants, then
reuse those sets for membership tests instead of allocating arrays per node. In
the managedImage capability calculation, reuse the existing classTokens computed
near line 139 to detect aria-managed-image rather than rescanning
opening.attributes and splitting the class value again.

In `@aria/lib/rendering/canonical/compileRenderDocument.ts`:
- Around line 67-113: Extract options.iconResources ??
options.document.iconResources into a local fallback value near the start of the
render flow, then reuse it for the documentOptions, regionIconResources, and
compileCanonicalNodes freshIconResources calls. Remove the repeated fallback
expressions while preserving the existing resource-refresh behavior.

In `@aria/tests/cache/public-page-cache.test.ts`:
- Around line 9-14: Remove the vi.mock for getCloudflareEnv from the cache test,
since the purge path no longer consumes Cloudflare credentials. Retain the mock
only if another import in this file still invokes getCloudflareEnv.

In `@aria/tests/core/useSavePublish.test.ts`:
- Around line 404-430: Update the test around handleSaveAndPublish to assert
that pending delivery invokes deliveryStatusMock with the returned deliveryJobId
"public-route:publish:page-home:v2". Keep the existing toast assertions and
verify the polling contract implemented by pollPublishDelivery.

In `@aria/tests/features/stage/canvasDropInsertion.test.ts`:
- Around line 26-51: Update mockFlexParent so style configuration is associated
with each created parent and document.defaultView.getComputedStyle dispatches
based on the element argument, rather than replacing shared defaultView per
call. Preserve the existing display, flex, direction, and grid style values for
each mock element, and add an afterEach cleanup that restores the original
document.defaultView after every test.

In `@aria/tests/features/stage/useCanvasDrop.test.ts`:
- Around line 79-108: Make the useCanvasDrop test select the expected feedback
animation callback without depending on Map insertion order, by recording or
otherwise identifying the callback scheduled by scheduleDropFeedback and
invoking that callback explicitly. Also register the
canvas:add-elements-insertion and canvas:drop listeners with an AbortController
signal, then abort it in afterEach so listeners do not leak into the second
test.

In `@aria/tests/inputs/ListProperty.test.ts`:
- Around line 608-637: Extract the duplicated Tabs, TabsList, and TabsTrigger
definitions into one shared stub factory used by both test blocks around the
existing definitions and the corresponding block near the second location.
Ensure the factory consistently provides both the reactive modelValue and select
through tabsSelectionKey, while preserving the existing props, emits, rendering,
and click behavior.

In `@aria/tests/integration/publishing-snapshot-injection.test.ts`:
- Around line 99-104: Add a regression test in the publishing snapshot injection
suite for a concurrent draft save during publish. Configure
mockGetPageVersionPins to return a draftVersion different from the
just-published version while preserving the published and current versions, then
assert the publishing action’s reconciliation behavior for that mismatch. Cover
the existing setup shared by the referenced mock configuration without changing
matching-version scenarios.

In `@aria/tests/rendering-foundation/canonical-document.parity.test.ts`:
- Around line 187-189: Document how to regenerate the pinned documentHash near
the assertion in the canonical document parity test, including the exact
regeneration command or a named constant with that note. Preserve the existing
hash assertion and the determinism and self-consistency checks.
- Around line 196-199: Add negative test coverage in the canonical render
document parity suite: modify a field on the document and assert
hasValidRenderDocumentHash returns false, and compile a document with freeze:
false then assert the returned document is mutable. Keep the existing valid-hash
and frozen-document assertions unchanged.

In `@aria/tests/rendering-parity/canvas-parity.spec.ts`:
- Line 442: Extend the parity test after the existing
expectMountedListSemantics(stageFrame) call to run the same helper against the
public Frame returned by openSurface. Keep the stageFrame assertion and validate
both surfaces so canonical HTML list semantics are covered.

In `@aria/tests/rendering-parity/p0CanvasFixture.ts`:
- Around line 142-149: Clarify the intentional `ordered: true` setting on the
`parity-description-list` fixture in the `parity-description-list` node: either
remove the unused flag or add a brief comment explaining that it guards against
future tag resolution preferring `ordered` over `element`.

In `@aria/tests/storage/surface-normalization.test.ts`:
- Around line 38-56: Add a test alongside the existing
resolveStoredSemanticSourceHash case that derives the expected hash via
normalizeSurfaceForPersistence("page", dsl).sourceHash, stores that value as
content_hash with the renderSourceHashVersion marker, and asserts the resolver
returns the canonical hash without invoking fallback. Use valid DSL input so the
assertion verifies canonicalization rather than arbitrary hash pass-through.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

Comment on lines +340 to +354
const delivery = await deliverCommittedInvalidation(
adapter,
context,
invalidationJob.id,
);
const versionPins = await adapter.getPageVersionPins(currentPage.id);
if (
versionPins?.draftVersion !== publishedVersion ||
versionPins.publishedVersion !== publishedVersion
) {
throw new Error(
"Published page revision could not be reconciled with its saved draft.",
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repository files of interest:"
git ls-files | rg '(^|/)aria/(actions/publishing\.(ts|tsx)|lib/storage/internal/domains/pageLifecycle\.(ts|tsx))$' || true

echo
echo "Locate symbols:"
rg -n "publishMetaStatement|getPageVersionPins|publishPageDSL|savePageDSL|publishMetaStatement|current_version|published_version|draft_version" -S .

Repository: ariabuilder/aria

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "publishing.ts relevant section:"
sed -n '280,370p' aria/actions/publishing.ts

echo
echo "pageLifecycle publishPageDSL/publishMetaStatement relevant section:"
sed -n '750,910p' aria/lib/storage/internal/domains/pageLifecycle.ts

echo
echo "pageLifecycle savePageDSL relevant section:"
sed -n '260,370p' aria/lib/storage/internal/domains/pageLifecycle.ts

echo
echo "pageLifecycle getPageVersionPins relevant section:"
sed -n '1120,1260p' aria/lib/storage/internal/domains/pageLifecycle.ts

Repository: ariabuilder/aria

Length of output: 17170


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "actions/pages savePageDSL with expectedVersion:"
sed -n '230,295p' aria/actions/pages.ts

echo
echo "publishing.ts post-publish catch flow:"
sed -n '360,410p' aria/actions/publishing.ts

echo
echo "PageSaveOptions expectedVersion usages in actions:"
rg -n "expectedVersion|savePageDSL\\(" aria/actions/pages.ts aria/actions/_shared.ts -C 3

echo
echo "getVersionPins full query/read behavior:"
rg -n "async getPageVersionPins|resolvePageIdentity|publishVersionTableInsert|draft_version|published_version|current_version" aria/lib/storage/internal/domains/pageLifecycle.ts -C 4

Repository: ariabuilder/aria

Length of output: 22450


Post-publish draftVersion reconciliation should not fail publish.

publishPageDSL sets published_version while savePageDSL updates draft_version and current_version. A concurrent autosave can commit a newer draft after publishing succeeds, so comparing versionPins.draftVersion !== publishedVersion here does not show an inconsistent publish state. This branch throws while the page is already published in storage, and the catch converts it to success: false with PUBLISH_FAILED. Use a non-fatal check for draft divergence or remove the draft-version comparison.

🤖 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 `@aria/actions/publishing.ts` around lines 340 - 354, The post-publish
reconciliation in publishPageDSL must not fail when draftVersion differs from
publishedVersion because of a concurrent autosave. Remove draftVersion from the
fatal condition or make that divergence non-fatal, while retaining validation
that publishedVersion matches the expected publishedVersion and avoiding
conversion of a successful publish into PUBLISH_FAILED.

Comment on lines +413 to +428
deliveryStatus: defineAction({
accept: "json",
input: z.object({ jobId: z.string().trim().min(1).max(160) }).strict(),
handler: async ({ jobId }, context) => {
await requireOperation(context, "publishing.publish");
const adapter = await getStorageAdapterAsync(context.locals);
const delivery = await deliverCommittedInvalidation(
adapter,
context,
jobId,
true,
);
return { success: true, data: { jobId, delivery } };
},
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

deliveryStatus bypasses backoff on every poll.

This handler always calls deliverCommittedInvalidation(..., true), so claimDueCacheInvalidationJobs skips the next_attempt_at <= ? gate on every invocation. After any failed attempt clears its lease, the very next poll immediately re-attempts delivery (up to 3 sub-attempts in drainCacheInvalidations) with no respect for the computed backoff window. If a client polls this action while the underlying cache-purge endpoint is degraded, this can turn into a retry storm against that endpoint instead of backing off.

Respect the job's nextAttemptAt unless a caller explicitly requests an immediate retry, or apply a minimum interval between forced attempts for the same job.

🤖 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 `@aria/actions/publishing.ts` around lines 413 - 428, Update the deliveryStatus
handler to stop unconditionally forcing delivery via the true argument to
deliverCommittedInvalidation. Respect the job’s nextAttemptAt backoff by
default, and only bypass it when the API explicitly accepts and validates an
immediate-retry request; otherwise preserve normal delivery behavior.

Comment on lines +47 to +52
async function awaitTrackedCommits(): Promise<void> {
// Commits may enqueue follow-up mutations, so wait until the set is stable.
while (pendingCommits.size > 0) {
await Promise.all([...pendingCommits]);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound the settle loop.

awaitTrackedCommits repeats while pendingCommits is non-empty. A commit that enqueues a follow-up commit on every settle keeps the set non-empty forever. Save and publish then never resolve, and loadingState.isSaving stays true. Add an iteration bound so the barrier fails or continues instead of spinning.

🛡️ Proposed bound
 async function awaitTrackedCommits(): Promise<void> {
   // Commits may enqueue follow-up mutations, so wait until the set is stable.
-  while (pendingCommits.size > 0) {
-    await Promise.all([...pendingCommits]);
-  }
+  for (let pass = 0; pass < 10 && pendingCommits.size > 0; pass += 1) {
+    await Promise.all([...pendingCommits]);
+  }
+  if (pendingCommits.size > 0) {
+    throw new Error("Editor commits did not settle");
+  }
 }
📝 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
async function awaitTrackedCommits(): Promise<void> {
// Commits may enqueue follow-up mutations, so wait until the set is stable.
while (pendingCommits.size > 0) {
await Promise.all([...pendingCommits]);
}
}
async function awaitTrackedCommits(): Promise<void> {
// Commits may enqueue follow-up mutations, so wait until the set is stable.
for (let pass = 0; pass < 10 && pendingCommits.size > 0; pass += 1) {
await Promise.all([...pendingCommits]);
}
if (pendingCommits.size > 0) {
throw new Error("Editor commits did not settle");
}
}
🤖 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 `@aria/admin/features/Core/composables/editorCommitCoordinator.ts` around lines
47 - 52, Bound the loop in awaitTrackedCommits so repeatedly enqueued follow-up
commits cannot keep the barrier running indefinitely. Track iterations and stop
after a finite limit, using the existing error or continuation behavior
appropriate for save and publish so loadingState.isSaving can resolve instead of
remaining stuck.

Comment on lines +74 to +76
const failure = commitFailures.shift();
commitFailures.length = 0;
if (failure) throw failure;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Stale commit failures can abort an unrelated later save.

commitFailures is module scoped and only drained inside the barrier. A commit that fails while no save or publish runs leaves an error in the array. The next settleEditorCommitBarrier call throws that old error, even for a different document or after the user already recovered. The barrier also discards every failure after the first.

Consider clearing failures when the tracked commit set becomes empty, or attach a sequence token so the barrier only consumes failures from commits that were pending during that barrier.

🤖 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 `@aria/admin/features/Core/composables/editorCommitCoordinator.ts` around lines
74 - 76, Update the module-scoped commit failure handling used by
settleEditorCommitBarrier so failures cannot survive an empty tracked commit set
and abort a later unrelated save. Clear or scope failures when the tracked
commits become empty, and preserve all relevant failures instead of discarding
entries after the first; ensure the barrier only throws failures associated with
commits pending for that barrier.

Comment on lines +1857 to +1874
saveProperty: (...args) =>
isEditorMutationLocked()
? Promise.resolve(false)
: trackEditorCommit(saveProperty(...args), "Property change"),
saveProperties: (...args) =>
isEditorMutationLocked()
? Promise.resolve(false)
: trackEditorCommit(saveProperties(...args), "Property changes"),
previewStyleProperties: (...args) =>
isEditorMutationLocked() ? false : previewStyleProperties(...args),
previewResponsiveStyleUpdates: (...args) =>
isEditorMutationLocked() ? false : previewResponsiveStyleUpdates(...args),
previewProps: (...args) =>
isEditorMutationLocked() ? false : previewProps(...args),
saveNodeUpdates: (...args) =>
isEditorMutationLocked()
? Promise.resolve(false)
: trackEditorCommit(saveNodeUpdates(...args), "Node property change"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Benign false results become commit failures.

trackEditorCommit records a failure whenever the tracked promise resolves to false (see aria/admin/features/Core/composables/editorCommitCoordinator.ts lines 25-27). saveProperty, saveProperties, and saveNodeUpdates also return false for non-error conditions: no node selected (Line 1294), missing itemType/itemSlug (Line 1303), and rejected input validation (Line 1318). After any of those, the next settleEditorCommitBarrier call throws "Property change could not be committed" and blocks the save or publish.

Track only real commits, or make the coordinator treat false as a no-op and rely on thrown errors for failures.

🤖 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 `@aria/admin/features/Core/composables/usePropertySave.ts` around lines 1857 -
1874, Update the commit-tracking flow around saveProperty, saveProperties, and
saveNodeUpdates so benign false results from validation or missing context do
not become commit failures. Adjust trackEditorCommit or these wrappers to track
only successful real commits, while preserving thrown errors as failures and
keeping the existing lock behavior unchanged.

Comment on lines +99 to +101
if (normalizedType === "svg") {
return [{ type: "trusted-svg", value: content }];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find every consumer of trusted-svg children.
rg -n -C5 "trusted-svg" --type=ts --type=vue

Repository: ariabuilder/aria

Length of output: 154


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
git ls-files | sed -n '1,200p'

echo
echo "Locate compileCanonicalNode.ts:"
fd -a 'compileCanonicalNode\.ts$' . || true

echo
echo "Search canonical-related files:"
git ls-files | rg 'canonical|render' | sed -n '1,200p'

Repository: ariabuilder/aria

Length of output: 13554


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "compileCanonicalNode.ts outline:"
ast-grep outline aria/lib/rendering/canonical/compileCanonicalNode.ts --view expanded || true

echo
echo "Relevant compileCanonicalNode.ts lines 1-180:"
sed -n '1,180p' aria/lib/rendering/canonical/compileCanonicalNode.ts | cat -n

echo
echo "Search all files for trusted-svg and CanonicalRenderChild occurrences:"
rg -n -C4 "trusted-svg|CanonicalRenderChild" aria || true

Repository: ariabuilder/aria

Length of output: 11936


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "contract.ts relevant lines:"
sed -n '220,275p' aria/lib/rendering/canonical/contract.ts | cat -n

echo
echo "index.ts relevant lines around canonical rendering contract:"
sed -n '70,115p' aria/lib/rendering/canonical/index.ts | cat -n

echo
echo "Search raw content sinks and CanonicalRenderChild consumers across all tracked files:"
rg -n -C3 "trusted-svg|CanonicalRenderChild|innerHTML|insertAdjacentHTML|setHTML|innerHTML:" . || true

Repository: ariabuilder/aria

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "svg raw sanitizer implementation:"
fd -a '.*svg.*sanitize.*|sanitizeSvg|svgSanitizer' aria . | sed -n '1,120p'
rg -n -C4 "sanitizeSvg|svg sanitizer|svgSanitizer|trusted-svg|trusted.*svg|rawContent\\(.*svg|svg.*innerHTML" aria --type=ts --type=vue --type=js | sed -n '1,240p'

echo
echo "focused nodeChildren call sites in canonical:"
rg -n -C4 "nodeChildren\\(|compileCanonicalNode\\(|compileCanonicalNodes\\(" aria/lib/rendering/canonical --type=ts

Repository: ariabuilder/aria

Length of output: 9430


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "SvgProperty stripUnsafeSvg implementation:"
sed -n '220,390p' aria/admin/features/Inspector/inputs/SvgProperty.vue | cat -n

echo
echo "StageFrame svg handling:"
sed -n '1920,1980p' aria/admin/features/Stage/components/StageFrame.vue | cat -n

echo
echo "Render html-to-svg parsing:"
rg -n -C4 "type: \"svg\"|type: 'svg'|SvgNodePropsSchema|parseSvgMarkup|svg:|is svg" aria/lib aria/admin --type=ts --type=vue | sed -n '1,260p'

Repository: ariabuilder/aria

Length of output: 28389


Sanitize trusted-svg before exporting it.

nodeChildren assigns ScalarText from props.content, props.text, or props.label to trusted-svg; if the node is svg, this content includes unvalidated SVG markup. Sanitize SVG content such as script elements, event-handler attributes, and javascript: URLs before returning the child, instead of encoding it as trusted markup.

🤖 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 `@aria/lib/rendering/canonical/compileCanonicalNode.ts` around lines 99 - 101,
Update the normalizedType === "svg" branch in compileCanonicalNode to sanitize
content before constructing the trusted-svg child. Reuse the project’s existing
SVG sanitization utility if available, ensuring script elements, event-handler
attributes, and javascript: URLs are removed while preserving valid SVG markup.

Comment on lines +111 to +121
const html = compileCanonicalNodeHtml(
node,
0,
breakpoints,
styleMode,
null,
{},
options.iconResources,
);
const opening = parseOpeningElement(html);
if (!opening) return null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether compileCanonicalNodeHtml can emit an opening tag only.
fd -t f 'renderDocumentHtml.ts' aria/lib/rendering | while IFS= read -r f; do
  ast-grep outline "$f" --items all
done

Repository: ariabuilder/aria

Length of output: 4773


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the canonical compile path and related caller.
sed -n '70,180p' aria/lib/rendering/canonical/compileCanonicalNode.ts
printf '\n--- compileCanonicalNodeHtml signature/body ---\n'
sed -n '870,1210p' aria/lib/rendering/canonical/renderDocumentHtml.ts | sed -n '1,220p'
printf '\n--- compileCanonicalNodes references ---\n'
rg -n "function compileCanonicalNodes|compileCanonicalNodes\\(|compileCanonicalNode\\(|parseOpeningElement" aria/lib/rendering/canonical

Repository: ariabuilder/aria

Length of output: 11726


Avoid full-subtree HTML serialization per canonical node.

compileCanonicalNode serializes node through the full compileCanonicalNodeHtml path just to parse the opening tag, then compileCanonicalNodes recurses into node.children and repeats that serialization for every descendant. Use an opening-tag-only path or a single tree serialization/walk to avoid quadratic compileCanonicalNodes work.

🤖 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 `@aria/lib/rendering/canonical/compileCanonicalNode.ts` around lines 111 - 121,
Update compileCanonicalNode and compileCanonicalNodes to avoid calling
compileCanonicalNodeHtml for each node’s full subtree solely to obtain its
opening element. Use an opening-tag-only compilation path or serialize and walk
the tree once, while preserving the existing opening parsing and recursive
output behavior.

Comment on lines +37 to +46
function extractHeadHtml(html: string): string {
return html.match(/<head>([\s\S]*?)<\/head>/iu)?.[1]?.trim() ?? "";
}

function extractStylesheet(html: string): string {
return Array.from(html.matchAll(/<style(?:\s[^>]*)?>([\s\S]*?)<\/style>/giu))
.map((match) => match[1]?.trim() ?? "")
.filter(Boolean)
.join("\n\n");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Structure is discarded into an HTML string and then recovered by regex in two places. The canonical pipeline holds the head nodes, the stylesheet, and the per-region body markup, serializes them into one HTML string, and both sites then parse that string back with tag patterns. Both patterns assume a bare <head>, stop at the first closing tag, and cannot distinguish generated content from author-supplied content.

  • aria/lib/rendering/canonical/compileRenderDocument.ts#L37-L46: return the head nodes and the generated stylesheet from nodesToHtmlDocument and assign them directly, instead of calling extractHeadHtml and extractStylesheet on the serialized output.
  • aria/lib/rendering/renderPageHtml.ts#L156-L160: expose the compiled RenderDocumentV1 (or its head and body parts) from renderPageDslToHtml, then build headHtml and bodyHtml from those fields instead of matching <head> and <body> in rendered.html.
📍 Affects 2 files
  • aria/lib/rendering/canonical/compileRenderDocument.ts#L37-L46 (this comment)
  • aria/lib/rendering/renderPageHtml.ts#L156-L160
🤖 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 `@aria/lib/rendering/canonical/compileRenderDocument.ts` around lines 37 - 46,
Replace the regex-based extraction in
aria/lib/rendering/canonical/compileRenderDocument.ts lines 37-46 by returning
and assigning the head nodes and generated stylesheet directly from
nodesToHtmlDocument, preserving the compiled structure. In
aria/lib/rendering/renderPageHtml.ts lines 156-160, expose the compiled
RenderDocumentV1 or its head/body fields from renderPageDslToHtml and construct
headHtml and bodyHtml from those fields instead of matching tags in
rendered.html.

Comment on lines +1147 to +1161
if (
normalizedNodeType === "list" &&
(!node.children || node.children.length === 0) &&
Array.isArray(renderProps.items) &&
renderProps.items.length > 0
) {
const listItemsHtml = renderProps.items
.map(
(item) =>
`${indentStr} <li>${escapeHTML(typeof item === "string" ? item : String(item ?? ""))}</li>`,
)
.join("\n");

return `${indentStr}${openTag(tag, attrsStr)}\n${listItemsHtml}\n${indentStr}</${tag}>`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

props.items handling for dl lists differs between the canonical renderer and Stage. Only Stage skips item generation when the resolved tag is dl. The canonical renderer still emits li children, so a description list that carries props.items renders invalid <dl><li>…</li></dl> in published HTML while Stage renders an empty dl.

  • aria/lib/rendering/canonical/renderDocumentHtml.ts#L1147-L1161: add tag !== "dl" to the condition so li items are not emitted inside a dl.
  • aria/admin/features/Stage/components/StageFrame.vue#L1865-L1868: keep the existing dl guard and confirm it matches the canonical rule after the renderer fix.
📍 Affects 2 files
  • aria/lib/rendering/canonical/renderDocumentHtml.ts#L1147-L1161 (this comment)
  • aria/admin/features/Stage/components/StageFrame.vue#L1865-L1868
🤖 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 `@aria/lib/rendering/canonical/renderDocumentHtml.ts` around lines 1147 - 1161,
The canonical list-item generation condition in
aria/lib/rendering/canonical/renderDocumentHtml.ts lines 1147-1161 must also
require the resolved tag not be "dl", preventing li children inside description
lists. Keep the existing dl guard in
aria/admin/features/Stage/components/StageFrame.vue lines 1865-1868 unchanged;
it already matches the corrected canonical behavior.

Comment on lines +214 to +249
{
id: "parity-unordered-list",
type: "List",
props: {
"data-parity-id": "unordered-list",
element: "ul",
},
styles: {},
children: [
{
id: "parity-unordered-list-item",
type: "ListItem",
props: { element: "li", content: "Unordered item" },
styles: {},
children: [],
},
],
},
{
id: "parity-ordered-list",
type: "List",
props: {
"data-parity-id": "ordered-list",
ordered: true,
},
styles: {},
children: [
{
id: "parity-ordered-list-item",
type: "ListItem",
props: { element: "li", content: "Ordered item" },
styles: {},
children: [],
},
],
},

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C6 'P0_CANVAS_PARITY_NODE_IDS' aria

Repository: ariabuilder/aria

Length of output: 3221


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== p0CanvasFixture around exports =="
sed -n '240,290p' aria/tests/rendering-parity/p0CanvasFixture.ts

echo
echo "== browserSnapshotCollector outline/usages =="
rg -n -C4 'parityIds|data-parity-id|collectBrowserSurfaceSnapshot|parityIds:' aria/tests/rendering-parity aria -g "*.ts"

echo
echo "== candidate collector file size =="
wc -l aria/tests/rendering-parity/browserSnapshotCollector.ts aria/tests/rendering-parity/canvas-parity.spec.ts aria/tests/rendering-parity/p0CanvasFixture.ts

Repository: ariabuilder/aria

Length of output: 36753


Include the new parity node ids in P0_CANVAS_PARITY_NODE_IDS.

P0_CANVAS_PARITY_NODE_IDS is passed to collectBrowserSurfaceSnapshot and drives per-node parity collection. Add description-list, unordered-list, and ordered-list so the new list fixtures are covered by the P0 parity checks.

🤖 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 `@aria/tests/rendering-parity/p0CanvasFixture.ts` around lines 214 - 249, Add
the new fixture IDs—parity-description-list, parity-unordered-list, and
parity-ordered-list—to P0_CANVAS_PARITY_NODE_IDS so
collectBrowserSurfaceSnapshot includes all three list nodes in P0 parity
collection.

@andycamara
andycamara merged commit cbcabe9 into main Aug 3, 2026
4 checks passed
@andycamara
andycamara deleted the feature/render-v2-canonical-document branch August 3, 2026 20:34
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