Skip to content

feat: add experience-auditor showcase example [EXT-7476]#11010

Merged
Jared Jolton (jjolton-contentful) merged 25 commits into
masterfrom
feat/experience-auditor-showcase
Jul 9, 2026
Merged

feat: add experience-auditor showcase example [EXT-7476]#11010
Jared Jolton (jjolton-contentful) merged 25 commits into
masterfrom
feat/experience-auditor-showcase

Conversation

@jjolton-contentful

@jjolton-contentful Jared Jolton (jjolton-contentful) commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Note

This example targets an App SDK location that's currently in limited/internal availability. The Experience Editor toolbar location isn't generally available yet, so an app built from this example won't be installable in most spaces for the time being. The example is published now so the code and patterns are here to explore and build against ahead of a wider rollout.

Especially curious? If you'd like to try the toolbar location early, reach out to Contentful support — we're happy to talk through early-access options.

EXT-7476 · sibling of EXT-7365 under epic EXT-6925

Summary

  • Adds examples/experience-auditor — a polished, customer-facing showcase app for the Experience Editor toolbar location that goes well beyond the minimal starter (PR feat: add experience-toolbar starter example [EXT-7365] #11009 / EXT-7365). Scaffoldable via npx create-contentful-app --example experience-auditor.
  • Experience Auditor runs inside the Experience Editor toolbar and continuously audits the experience for accessibility, SEO, and content-completeness issues — demonstrating the standout capability of this location: live, selection-aware tooling that reads the experience tree as the author works and surfaces actionable, advisory fixes.

What it demonstrates

  • Live audit — walks the tree via getRootNodes()getProperties(), re-running on sdk.experiences.experience.onChange(). Five rules ship: missing alt text, empty required headings, empty SEO metadata, broken entry bindings (structural entryId check), and a cross-node heading-order check that flags skipped levels (e.g. H2 → H4).
  • Scored dashboard — a 0–100 health score (rendered as a ring gauge) with findings grouped by severity into error / warning / info.
  • Locate on canvasselection.set() + selection.highlight(nodeId, { flash, scrollIntoView }) jumps to the offending component when the host backs the selection surface; gracefully disabled with an inline explanation otherwise.
  • Advisory fixes — each finding can carry a suggested repair (e.g. an SEO meta value derived from the component's heading, or a sequential level for a skipped heading) surfaced as read-only advice for the author to apply manually. The app-sdk surface exposes no content-write call, so the example never mutates content.
  • Pre-publish gateexperience.publish() (permission-checked via sdk.access.can(), confirmed through sdk.notifier) is blocked while any error-level finding remains.

Design

Audit rules are pure functions over an SDK-independent CollectedNode shape (src/audit/rules.ts), so they are fully unit-tested without a live SDK. SDK coupling is confined to a thin collector (src/audit/collect.ts), which walks the tree read-only via getRootNodes()/getProperties(), and a capability probe (src/audit/capabilities.ts), which detects what the host backs (e.g. selection). Shared key-matching lives in src/audit/keys.ts; suggested-fix derivation is a separate pure module (src/audit/fixes.ts). Adding a rule = dropping another AuditRule into AUDIT_RULES (or, for an order-sensitive check, an evaluator the engine runs over the full node list).

A dev-only demo mode (npm start, then open /?demo) renders the panel against a seeded in-memory sdk.experiences, so the audit → suggested-fix → re-score loop is clickable without a live host. The demo scaffolding is dynamically imported and stripped from production builds.

Typed with ExperienceEditorToolbarAppSDK from @contentful/app-sdk@^4.63.1. Follows repo conventions (React + TS + Vite + Vitest + Forma 36), mirroring examples/typescript.

Verification status

  • tsc --noEmit clean against the published app-sdk types.
  • vite build clean.
  • 37/37 tests pass: rule/scoring, collector, suggested-fix derivation, capability detection, and toolbar integration paths (render-with-score, publish-gate, locate, advisory suggested-fix rendering, empty-tree vs. all-clear states).
  • ⚠️ Not runtime-verified inside a live Experience Editor — the host renderer serving sdk.experiences is still rolling out (EXT-7363/EXT-7364). README states this. API shapes match the published types exactly.

Test plan

  • npm install && npm run build in examples/experience-auditor succeeds
  • npm run test:ci passes
  • npx create-contentful-app --example experience-auditor scaffolds a working project (after merge to master)
  • (When host renderer lands) install, register the experience-toolbar location, open an experience with a missing-alt-text image, confirm the finding appears, Locate highlights the node, and publish unblocks once errors are resolved

Generated with Claude Code

@lewisjcs

Josh Lewis (lewisjcs) commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

⚔️ The Gauntlet — ✅ 0 Blockers · all advisory findings resolved

🤖 AI-powered · your code, through the lanes

gauntlet verdict

Note

Delta re-review — 2026-07-09, 57556b2c0. All six findings from the SDK-migration round are resolved by 57556b2c0 — the AutoFix JSDoc rewrite, the broken-binding finding text, the README demo copy, the empty-tree "no data yet" state, the dead propertyKey removal, and the version-specific test title. One small new drift the fix itself introduced: adding the no-tree-data test moved the suite 36 → 37, so the README's "36 tests" line (README.md:178) now undercounts by one. The PR body's count is correct. Non-blocking.

🛑 Blockers 0
⚠️ Concerns 4 → ✅ all resolved
💡 Nits 2 → ✅ all resolved
🔒 Security clean ✓
🧪 Lanes code-quality · adversarial · security

Findings from the original review (2026-06-04) and enhancement re-review (2026-06-08)

Lens Finding Location Status
⚠️ adversarial-review / Blast Radius README says the fix path is tested, but no fix-apply test exists — only the locate, publish-gate, and form-mode toolbar tests do. README.md:108 ✅ Moot (write path removed)
⚠️ adversarial-review / Hidden Assumptions IMAGE_KEY_HINT matches non-image keys like assetId/iconName/logoText, so a component with such a field and no alt sibling emits a false error that also blocks the publish gate. src/audit/rules.ts:5 ✅ Resolved
⚠️ doc-review / Accuracy of references The PR body's test-plan parenthetical lists a fix test that isn't among the four toolbar tests. PR body, Verification status ✅ Moot (write path removed)
⚠️ adversarial-review / Failure Scenarios onChange → audit() has no debounce; form-mode rapid edits fan out to N+1 SDK calls per burst. src/locations/ExperienceToolbar.tsx:88 ➖ By design (comment added)
💡 adversarial-review / Hidden Assumptions metaTitle/seoTitle match both the heading and meta hints, so one empty field double-fires two findings with no cross-rule dedup. src/audit/rules.ts ✅ Resolved
💡 adversarial-review / Failure Scenarios handleFix re-audits twice — setContentProperty fires onChangeaudit(), then handleFix also await audit(). src/locations/ExperienceToolbar.tsx ✅ Moot (handleFix removed)
⚠️ adversarial-review / Failure Scenarios SuggestedFix shows a stale suggestion across re-audits — the input seeds once from props and never re-syncs. src/components/SuggestedFix.tsx:30 ✅ Moot (editable input removed)
💡 code-quality-standards / Pattern-matching Hardcoded hex colors in SuggestedFix.tsx vs the sibling f36-tokens convention. src/components/SuggestedFix.tsx ✅ Resolved
💡 code-quality-standards / Concise Unused _metaKey parameter on suggestMetaFromHeading. src/audit/fixes.ts:12 ✅ Resolved
💡 code-quality-standards / Gaps "Stable rule set" test undersells the inventory — heading-order isn't in the asserted list. src/audit/audit.spec.ts ➖ By design (comment added)

New findings — SDK migration re-review (2026-07-09)

Lens Finding Location Status
⚠️ adversarial-review / Hidden Assumptions The AutoFix JSDoc still describes a write-and-confirm flow ("applied on click", "written after the author confirms") that no longer exists — the real SDK has no content-write call, and every other doc in this migration was updated to say fixes are read-only advisory except this one. src/audit/types.ts:16-24 ✅ Resolved in 57556b2c0 — JSDoc rewritten: both kinds "read-only".
⚠️ adversarial-review / Failure Scenarios The broken-binding finding's text still says "the reference does not resolve," but the check is now purely structural (!binding.entryId) — there's no way to verify the entry actually exists, only that an id string is present. src/audit/rules.ts:208 ✅ Resolved in 57556b2c0 — now "bound to an entry but has no entryId."
⚠️ adversarial-review / Failure Scenarios The README's demo instructions still say you can "accept or edit a suggested fix" — the editable input and Apply button were removed; the demo now only displays a read-only suggestion. README.md:151-152 ✅ Resolved in 57556b2c0 — now "click through findings, review a suggested fix".
⚠️ adversarial-review / Hidden Assumptions getRootNodes() currently resolves empty until the host wires up experience-tree sync (per the README's own new note) — but the toolbar renders the identical "all clear" UI for that case and for a genuinely clean experience, with no way to tell them apart. src/audit/collect.ts (via README.md:185-189) ✅ Resolved in 57556b2c0EmptyState renders a distinct no-tree-data Note when nodeCount === 0 (+ test).
💡 code-quality-standards / Concise propertyKey on AutoFix is dead data now that the write path (applyWrite) that consumed it is gone. src/audit/types.ts:26,30 ✅ Resolved in 57556b2c0 — dropped from both variants and all 3 call sites.
💡 adversarial-review / Hidden Assumptions A test title and comment still say "no write path on 4.59," though the pin moved to ^4.63.1 in this same range — the removal is permanent, not version-specific. src/locations/ExperienceToolbar.spec.tsx:62,64 ✅ Resolved in 57556b2c0 — version wording dropped from title + comment.

The Gauntlet clears this PR for merge — nothing here blocks. This cycle's diff is a mechanical SDK migration: the real, now-published sdk.experiences surface has no content-write call at all, so the entire one-click/suggested-fix write path was correctly removed in favor of read-only advice. That single change also resolved or mooted every finding from the last two review rounds — the alt-text false-positive, the token/param cleanup, and the double-audit and stale-suggestion findings all disappear along with the write path they depended on. tsc, the 36 tests, and vite build all verify clean against the actually-declared ^4.63.1 types (checked directly, not just the locally installed 4.60.0). What's left is doc/comment drift the migration missed in a few spots — the AutoFix JSDoc, one finding-text string, and the README's demo instructions still describe the old write-and-edit flow — plus a genuine UX ambiguity worth a look: right now a live host that hasn't wired up experience-tree sync yet looks identical to a fully clean experience.

One more thing worth a look before merge, outside the code lanes: the PR body's "41/41 tests pass" is stale — the suite is 36 tests now, matching the README.

🤖 Machine-readable findings (for agents)
{
  "tool": "gauntlet",
  "schema": "v1",
  "reviewed_ref": "d8784a7df",
  "verdict": { "blockers": 0, "concerns": 4, "nits": 2, "security": "clean" },
  "findings": [
    { "severity": "concern", "location": "src/audit/types.ts:16-24", "lens": "adversarial-review / Hidden Assumptions", "confidence": 88, "claim": "AutoFix JSDoc still describes the removed write-and-confirm flow, contradicting every other doc updated in this migration.", "recommendation": "Rewrite the JSDoc to describe both fix kinds as read-only advice." },
    { "severity": "concern", "location": "src/audit/rules.ts:208", "lens": "adversarial-review / Failure Scenarios", "confidence": 85, "claim": "brokenBindingRule's finding text says the reference 'does not resolve' but the check is now purely structural (missing entryId only).", "recommendation": "Change the detail text to describe a missing entryId, not an unresolved reference." },
    { "severity": "concern", "location": "README.md:151-152", "lens": "adversarial-review / Failure Scenarios", "confidence": 85, "claim": "README demo instructions describe an edit affordance (accept or edit a suggested fix) that no longer exists.", "recommendation": "Change to 'review a suggested fix' — no edit/accept action remains." },
    { "severity": "concern", "location": "src/audit/collect.ts", "lens": "adversarial-review / Hidden Assumptions", "confidence": 82, "claim": "A live host with no experience-tree sync yet renders identically to a genuinely clean experience — no UI signal distinguishes the two.", "recommendation": "Surface nodeCount === 0 as a distinct 'no data yet' state, not the same as a clean score." },
    { "severity": "nit", "location": "src/audit/types.ts:26,30", "lens": "code-quality-standards / Concise", "confidence": 80, "claim": "propertyKey on AutoFix is dead data since the write path that read it was removed.", "recommendation": "Drop propertyKey from both AutoFix variants." },
    { "severity": "nit", "location": "src/locations/ExperienceToolbar.spec.tsx:62,64", "lens": "adversarial-review / Hidden Assumptions", "confidence": 80, "claim": "Test title/comment still cite app-sdk 4.59 though the pin moved to ^4.63.1 in this range.", "recommendation": "Drop the version number — the write-path removal is permanent, not version-specific." }
  ]
}

🎮 The Gauntlet · an AI review harness built by Josh C.S. Lewis · reviewed at d8784a7df. Everything but the 🛑 is advisory.

@lewisjcs Josh Lewis (lewisjcs) force-pushed the feat/experience-auditor-showcase branch from 301da2a to 3d005cb Compare June 8, 2026 18:21
@lewisjcs Josh Lewis (lewisjcs) force-pushed the feat/experience-auditor-showcase branch from 24544c6 to d9d323b Compare June 24, 2026 19:13
@wiz-inc-38d59fb8d7

wiz-inc-38d59fb8d7 Bot commented Jul 6, 2026

Copy link
Copy Markdown

Wiz Scan Summary

Scanner Findings
Vulnerability Finding Vulnerabilities -
Data Finding Sensitive Data -
Secret Finding Secrets -
IaC Misconfiguration IaC Misconfigurations -
SAST Finding SAST Findings -
Software Management Finding Software Management Findings -
Total -

View scan details in Wiz

To detect these findings earlier in the dev lifecycle, try using Wiz Code VS Code Extension.

Add a polished, customer-facing example app for the ExO toolbar location
that goes beyond the minimal starter (EXT-7365). Experience Auditor runs
inside the Experience Editor toolbar and continuously audits the
experience for accessibility, SEO, and content-completeness issues.

It demonstrates the standout capability of the toolbar location: live,
selection-aware tooling that reads and mutates the experience tree. A
scored dashboard lists findings; clicking one locates the offending
component on the canvas (selection.set + selection.highlight), safe
deterministic fixes apply in place via setContentProperty (permission-
checked, with notifier feedback), and publish is gated on outstanding
errors.

Audit rules are pure functions over an SDK-independent node shape, fully
unit-tested without a live SDK; the SDK boundary is isolated in a thin
collector. Typed with ExperienceEditorToolbarAppSDK (app-sdk@4.58.0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…476]

Address code review on the experience-auditor showcase:
- alt-text rule now requires an asset-shaped value, not just an image-ish
  key, so string fields (iconName, logoText, assetId) no longer emit false
  errors that block the publish gate
- heading rule excludes meta-ish keys so metaTitle/seoTitle no longer
  double-fire across the heading and SEO rules
- add a fix-apply integration test (setContentProperty + re-audit) so the
  documented fix path is actually covered
- add regression tests for the false-positive and double-fire cases
- document the onChange re-audit no-debounce simplification and why
  handleFix re-audits explicitly

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…7476]

4.58.1 reshaped ComponentPropertyDescriptor.binding to the typed
`Binding` union (EntryBinding | ManualBinding), replacing the old
`{ sourceType, entryId }` ComponentPropertyBinding shape.

- rules.ts / collect.ts: gate entry bindings on `binding.type === 'entry'`
  (was `binding.sourceType === 'entry'`); `entryId` reads now narrow off
  the EntryBinding arm.
- Specs: use the EntryBinding fixture shape `{ type, entryId, fieldId }`.
- Bump @contentful/app-sdk 4.58.0 -> 4.58.1.

Verified: tsc --noEmit clean, 41 tests passing against 4.58.1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…7476]

- Bump @contentful/app-sdk 4.58.1 -> 4.58.2 to track the latest published ExO
  surface (DA definition-only trim, ui-extensions-sdk#2592).
- Run prettier (2.8.8, matching CI) on ConfigScreen.tsx to clear the
  prettier-check.sh violation failing apps-test.

41 tests green against 4.58.2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bump @contentful/app-sdk 4.58.2 → ^4.59.0 and adapt the example to the
removed surface:

- resolveEntryBinding is gone; brokenBindingRule now uses the structural
  !binding.entryId check it already fell back to (drop resolveBindings +
  ResolvedBinding type + the resolution-aware test cases).
- setContentProperty is gone; node content is DA-only on 4.59 with no
  content-string write path. The auditor becomes read-only: findings still
  surface their derived fix as advice (SuggestedFix renders the value
  read-only), but the one-click apply path is removed.

Auditor still detects + reports + locates; it no longer mutates content.
…le [EXT-7497]

floor bump ^4.60.0; Exo*→Experience* + sdk.exo→sdk.experiences; mockExo.ts→mockExperiences.ts.
…XT-7476]

The README described a write-capable "One-click fixes" feature (findings
writing back via getNode().setContentProperty()), but the app has no such
method and treats all fixes as read-only advice — the app-sdk surface exposes
no content-property write. Rewrites the headline, feature list, and fixes
section to match the code: fixes are surfaced as advice the author applies
manually. Also corrects the app-sdk version (4.58.0 -> 4.60.0, where
sdk.experiences first exists), drops customer-facing "ExO" phrasing, notes the
getRootNodes() empty-stub caveat, and fixes the test count (40 -> 37).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
app-sdk 4.63.1 removes the experience getUiMode()/onUiModeChanged()
surface. Drop the mode state, the form-mode notice, and the mode
condition on the Locate action, which now keys solely off the
capability probe for the selection surface. Bump the app-sdk pin to
^4.63.1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
- Rewrite AutoFix JSDoc and drop dead propertyKey field (write path gone)
- Fix broken-binding finding text to match structural entryId check
- README: demo is review-only; broken-binding table; no-tree-data note
- Distinguish empty getRootNodes() from a genuinely clean experience
- Drop version-specific wording from advisory-fix test title

Co-authored-by: Cursor <cursoragent@cursor.com>
@jjolton-contentful Jared Jolton (jjolton-contentful) merged commit 378c3ce into master Jul 9, 2026
16 checks passed
@jjolton-contentful Jared Jolton (jjolton-contentful) deleted the feat/experience-auditor-showcase branch July 9, 2026 20:58
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.

2 participants