Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 24 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ jobs:

- run: pnpm audit --audit-level=high

- name: Verify browser harness build cleanup
run: node --test scripts/fixtures/replay-process.test.mjs

- run: pnpm exec nx run-many -t lint test build typecheck --exclude domscribe-test-fixtures

- name: Update coverage badge
Expand Down Expand Up @@ -131,19 +134,35 @@ jobs:
- id: react-npm-vite7
framework: react
package-manager: npm
vite: '7.3.7'
version-args: '--vite-version=7.3.7'
- id: react-pnpm-vite5
framework: react
package-manager: pnpm
vite: '5.4.21'
version-args: '--vite-version=5.4.21'
- id: vue-npm-vite7
framework: vue
package-manager: npm
vite: '7.3.7'
version-args: '--vite-version=7.3.7'
- id: vue-pnpm-vite5
framework: vue
package-manager: pnpm
vite: '5.4.21'
version-args: '--vite-version=5.4.21'
- id: next15-npm-webpack
framework: next
package-manager: npm
version-args: '--framework-version=15.5.28'
- id: next16-pnpm-turbopack
framework: next
package-manager: pnpm
version-args: '--framework-version=16.4.3'
- id: nuxt3-npm
framework: nuxt
package-manager: npm
version-args: '--framework-version=3.21.11'
- id: nuxt4-pnpm
framework: nuxt
package-manager: pnpm
version-args: '--framework-version=4.6.2'
steps:
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
Expand All @@ -163,7 +182,7 @@ jobs:
node scripts/verify-instance-replay.mjs
--framework=${{ matrix.framework }}
--package-manager=${{ matrix.package-manager }}
--vite-version=${{ matrix.vite }}
${{ matrix.version-args }}
--report-dir=${{ runner.temp }}/portable-replay-evidence
- name: Retain replay evidence
if: always()
Expand Down
29 changes: 26 additions & 3 deletions docs/agent-targets-and-replay.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Exact instance selection and portable replay

These APIs are on the local `codex/instance-selection-replay` feature branch. They are not part of the published 0.6.1 packages yet.
These APIs are in the repository following PR #77. They are not part of the published 0.6.1 packages yet.

A `data-ds` ID identifies a source element. Repeated rows can share it. An `instanceId` identifies one live DOM instance and has a strict, transient browser selector. Its `locator` describes the target independently of that session.

Expand All @@ -16,7 +16,7 @@ The response contains `status`, `targets`, `total`, and `browserConnected`. Each

To inspect an instance, send `{"instanceId":"<returned instance ID>"}`. To locate a capture in a new session, send `{"locator":<saved locator>}`. `domscribe.query.bySource` also accepts an optional `instanceId` or `locator` and returns `selectionStatus`, `targets` and `total` alongside its existing fields.

Only `resolved` identifies one exact target and returns its available runtime context. `listed` is discovery, not a selection. `ambiguous`, `not_found`, `stale`, `wrong_page`, `not_ready`, `unsupported`, `ambiguous_browser`, and `unavailable` are explicit stopping conditions. Reconnect/retry readiness failures or refine ambiguous targets; do not choose the first candidate. Keep one browser session connected to the relay for these queries.
Only `resolved` identifies one exact target and returns its available runtime context. `listed` is discovery, not a selection. `ambiguous`, `not_found`, `stale`, `wrong_page`, `not_ready`, `unsupported`, `ambiguous_browser`, and `unavailable` are explicit stopping conditions. Reconnect/retry readiness failures or refine ambiguous targets; do not choose the first candidate. Keep one browser session connected to the relay for these queries. After HMR, the manifest can briefly lag newly rendered source IDs; retry source resolution while updates settle instead of substituting an old mapping.

Handles survive DOM moves. Removed/recycled targets and changed source or identity constraints invalidate them. Never persist a handle as the only identity for a later session. Browser drivers still check visibility, enabled state, overlays and normal actionability; discovery does not imply that an element can be clicked immediately.

Expand Down Expand Up @@ -71,6 +71,29 @@ NX_DAEMON=false pnpm exec nx run-many -t build,sync-dist \
node scripts/verify-instance-replay.mjs
node scripts/verify-instance-replay.mjs --framework=vue --package-manager=pnpm \
--vite-version=5.4.21 --report-dir=/tmp/vue-replay-evidence
node scripts/verify-instance-replay.mjs --framework=next --package-manager=pnpm \
--framework-version=16.4.3 --report-dir=/tmp/next-replay-evidence
node scripts/verify-instance-replay.mjs --framework=nuxt --package-manager=npm \
--framework-version=3.21.11 --report-dir=/tmp/nuxt-replay-evidence
```

The harness installs local tarballs into a temporary consumer, uses the fixture project's existing Playwright dependency and installed Chromium, persists an annotation, closes the browser context, reorders rows in a fresh context, and replays the same four UI steps through the relay. It also verifies source-only ambiguity, exact runtime props, committed editor state after real typing/checking, stale-handle refusal, ambiguity stopping, application page errors, and production DOM instrumentation removal. Evidence is retained in the printed temporary directory. The default remains React 18/npm/Vite 7. CI also verifies React 18/pnpm/Vite 5, Vue 3/npm/Vite 7, and Vue 3/pnpm/Vite 5, with at most two replay jobs running simultaneously. These are four representative combinations, not every framework/manager/version permutation. Each run checks the mapped framework source, actual installed versions, and shared local runtime; Vue uses compiled SFC components. Package installs use local tarballs, with explicit pnpm local-artifact overrides so unpublished branch artifacts cannot be replaced by registry copies. This is not evidence that the new APIs are in the published npm release. Logs and result JSON are retained on success and failure. Traces and screenshots are retained when available, with best-effort failure screenshots after browser startup. Production UI interactions are verified after instrumentation removal. Next/Nuxt browser coverage for the new APIs remains outstanding.
The harness installs local tarballs into a temporary consumer, uses the fixture project's existing Playwright dependency and installed Chromium, persists an annotation, closes the browser context, reorders rows in a fresh context, and replays the same four UI steps through the relay. Assertions cover exact mapped source/props, committed editor state after typing/checking, stale handles after unmount and across sessions, ambiguity refusal, application console/page/network failures, production instrumentation removal (including conditional editors), and correct production UI updates.

CI runs eight representative combinations, with at most two replay jobs simultaneously:

| Integration | Consumer versions | Package manager |
| --------------------------- | ------------------------- | --------------- |
| React/Vite | React18.3.1 / Vite7.3.7 | npm |
| React/Vite | React18.3.1 / Vite5.4.21 | pnpm |
| Vue/Vite | Vue3.5.44 / Vite7.3.7 | npm |
| Vue/Vite | Vue3.5.44 / Vite5.4.21 | pnpm |
| Next App Router / Webpack | Next15.5.28 / React19.3.1 | npm |
| Next App Router / Turbopack | Next16.4.3 / React19.3.1 | pnpm |
| Nuxt SSR | Nuxt3.21.11 / Vue3.5.44 | npm |
| Nuxt SSR | Nuxt4.6.2 / Vue3.5.44 | pnpm |

The default remains React18/npm/Vite7. Use `--framework-version` for exact Next15/16 or Nuxt3/4 releases; use `--vite-version` for React/Vue consumers. Mixed or unsupported flags are rejected. Native SSR consumers use their framework integrations and normal hydration, not the Vite SPA integration. Nuxt excludes the runtime from Vite prebundling so its native plugin and consumer queries share one runtime on cold startup. Each run checks actual installed versions, expected framework source, and local runtime resolution.

These are representative synthetic regression cases, not the full version/manager/router matrix or an adoption benchmark. Additional local testing on the Domscribe website (Next16.4.3 / React19.2.4) verified exact installation-tab source/state among six repeated instances, persisted replay after a fresh browser session, safe page-navigation failure, mobile navigation state/replay after reload, and controlled HMR identity changes without resurrecting retired handles. Its production build and UI also worked without instrumentation. This is one maintained app, not evidence for every router, virtualized list, framework version, or agent workflow advantage. Package installs use local tarballs, with explicit pnpm local-artifact overrides so unpublished artifacts cannot be replaced by registry copies. This is not evidence that the new APIs are in the published npm release.

Logs and result JSON are retained in the printed temporary directory on success and failure. `--report-dir` copies a compact evidence bundle. Traces and screenshots are retained when available, with best-effort failure screenshots after browser startup. Server startup and builds are bounded, and the harness stops its owned server process group and relay.
44 changes: 44 additions & 0 deletions docs/superpowers/plans/2026-10-25-next-nuxt-replay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Next and Nuxt Replay Verification Plan

> **For agentic workers:** Use superpowers:executing-plans inline; obtain one independent whole-branch review. Steps use checkboxes.

**Goal:** Close the Next/Nuxt exact-instance and portable replay browser gap, fixing reproduced integration defects with regression tests.

**Architecture:** Extend the existing isolated local-tarball consumer harness and equivalent customer-editor fixtures. Exercise native Next App Router SSR/hydration (15 Webpack, 16 Turbopack) and Nuxt SSR/hydration (3 and 4), with the same exact-source/props/state, fresh-session replay and production assertions. Keep framework setup and commands in a small helper.

**Tech Stack:** Existing Node/Playwright harness, Next15.5.28/16.4.3, React19.3.1, Nuxt3.21.11/4.6.2, Vue3.5.44; npm/pnpm.

**Spec:** `docs/superpowers/specs/2026-10-25-instance-selection-replay-design.md` and the user's authorized continuation of framework and realistic-app verification.

## Constraints and review focus

- Reuse isolated worktree on branch codex/next-nuxt-portable-replay, based on freshly fetched main2e0d92d. Preserve user checkouts and lockfiles.
- Keep default React/Vite verification working. No publication/deployment, credentials, settings or CI Action product work.
- Assert actual installed framework versions, expected source files, and committed component state after hydration; no framework-state injection.
- pnpm local-artifact overrides remain explicit; do not claim unmodified published-package success.
- Bound SSR startup/build/browser operations and clean owned process trees, including failures. Retain logs/screenshots.
- Verify conditional production elements and production UI actions; a replay success alone is insufficient.
- Add navigation/lifecycle evidence where it exposes a real gap. Report unsupported paths honestly.

## Task 1: Native SSR consumers

- [x] Baseline React consumer passes; current Next invocation fails because only React/Vue are accepted (RED).
- [x] Extend CLI with next/nuxt and exact framework-version, native config/source/commands, bounded builds and cleanup. Share fixture UI.
- [x] Run fresh Next15/npm, Next16/pnpm, Nuxt3/npm, Nuxt4/pnpm consumers. Record actual versions, mapped sources, all outcomes. Fix product failures only after reproducing them.

## Task 2: Regression delivery and readiness

- [x] Add representative bounded CI cases and accurate docs for passing paths; rerun default React/Vue regressions.
- [x] Run affected configured checks and obtain independent review; address material findings with regressions.
- [x] Push focused PR, inspect latest CI, and record status in the continuity ledger.
- [x] Update continuity ledger with exact evidence and remaining realistic-app/lifecycle gaps. Do not claim readiness or measured user advantage prematurely.

## Execution evidence

- Old harness Next invocation rejected (RED). Original React baseline passed seven checks.
- Native Next15/npm, Next16/pnpm and Nuxt4/pnpm initially passed; Nuxt3/npm consistently failed cold readiness. Diagnostic browser showed native Nuxt plugin importing raw runtime ESM and app query importing optimized runtime, with separate singletons. Isolated exclude hypothesis passed on a cold cache.
- Regression unit initially failed (1 failed /32 passed). Nuxt module now preserves existing optimizeDeps excludes and adds runtime exclusion idempotently after its dev guard. Configured Nuxt test/lint/build passed;41 tests. Fresh Nuxt3 cold consumer then passed eight checks. One attempted install hit /tmp inode exhaustion; retained failure evidence and cleaned completed temporary dependencies before fresh success.
- All eight representative consumer combinations passed eight browser checks each. No console/page/network errors. Latest process-helper reruns: Next16/pnpm, Nuxt3/npm and Vue/pnpm also passed eight each.
- Full uncached configured unit suite passed2114 tests in11 projects (test dependencies already built; excluded dependency tasks to avoid concurrent artifact mutation).
- Independent review found one P2: direct-child build timeout could leave workers. Reproduced surviving worker on timeout and cleaned it; replaced with detached owned-group async build, TERM/KILL deadline and retained logs. Two process regression tests pass; focused re-review has no material findings. Applied bounded polling to avoid a termination-signal race in its Linux test.
- PR78 pushed with fresh CI; inspect current checks in the continuity ledger. Real-site verification passed four dev capture/replay/navigation checks, three production checks, and four controlled HMR/mobile checks. HMR manifest propagation required bounded source-resolution retry; documented this limit. All temporary website source/config changes were restored; no website commit or deployment.
18 changes: 18 additions & 0 deletions packages/domscribe-nuxt/src/module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ interface MockNuxt {
options: {
dev: boolean;
rootDir: string;
vite: { optimizeDeps?: { exclude?: string[] } };
app: {
head: {
script: Array<{ innerHTML: string }>;
Expand Down Expand Up @@ -94,6 +95,7 @@ function createMockNuxt(overrides?: Partial<MockNuxt['options']>): MockNuxt {
options: {
dev: true,
rootDir: '/test/project',
vite: {},
app: {
head: {
script: [],
Expand Down Expand Up @@ -158,6 +160,22 @@ describe('domscribeModule', () => {
});

describe('setup', () => {
it('keeps one runtime module identity for native plugin and consumer queries on cold Vite startup', async () => {
const nuxt = createMockNuxt({
vite: { optimizeDeps: { exclude: ['existing-package'] } },
});
await callSetup({ relay: { autoStart: false } }, nuxt);
expect(nuxt.options.vite.optimizeDeps?.exclude).toEqual([
'existing-package',
'@domscribe/runtime',
]);
await callSetup({ relay: { autoStart: false } }, nuxt);
expect(nuxt.options.vite.optimizeDeps?.exclude).toEqual([
'existing-package',
'@domscribe/runtime',
]);
});

describe('dev-mode guard', () => {
it('should skip setup in production without force-transform', async () => {
const nuxt = createMockNuxt({ dev: false });
Expand Down
9 changes: 9 additions & 0 deletions packages/domscribe-nuxt/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ export const domscribeModule: NuxtModule<DomscribeNuxtOptions> =
const debug = options.debug ?? false;
const forceTransform = !!process.env.DOMSCRIBE_FORCE_TRANSFORM;

// Nuxt can import its native plugin's runtime as raw ESM while Vite
// prebundles an application's runtime import on a cold start. Those
// module copies have separate RuntimeManager singletons. Keep both
// imports on the same ESM path so queries see the initialized adapter.
const optimizeDeps = (nuxt.options.vite.optimizeDeps ??= {});
optimizeDeps.exclude = [
...new Set([...(optimizeDeps.exclude ?? []), '@domscribe/runtime']),
];

// 1. Start relay to discover actual host/port.
// Nuxt bypasses Vite's transformIndexHtml, so the Vite plugin's
// HTML injection never fires. We start the relay here and inject
Expand Down
56 changes: 55 additions & 1 deletion scripts/fixtures/replay-consumer.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import fs from 'node:fs';
import path from 'node:path';

export function writeReplayConsumer(consumer, framework) {
export function writeReplayConsumer(consumer, appFramework) {
const framework =
appFramework === 'next'
? 'react'
: appFramework === 'nuxt'
? 'vue'
: appFramework;
const source = path.join(consumer, 'src');
fs.mkdirSync(source);
const entry = framework === 'vue' ? 'main.js' : 'main.jsx';
Expand Down Expand Up @@ -96,4 +102,52 @@ createRoot(document.getElementById('root')).render(<App/>);
`,
);
}
if (appFramework === 'next') {
fs.rmSync(path.join(consumer, 'index.html'));
fs.rmSync(path.join(consumer, 'vite.config.mjs'));
const entry = path.join(source, 'main.jsx');
fs.writeFileSync(
entry,
"'use client';\n" +
fs
.readFileSync(entry, 'utf8')
.replace("import{createRoot}from'react-dom/client';", '')
.replace(
'if(import.meta.env.DEV)',
"if(typeof window!=='undefined' && process.env.NODE_ENV==='development')",
)
.replace('function App()', 'export default function App()')
.replace(
"createRoot(document.getElementById('root')).render(<App/>);",
'',
),
);
fs.mkdirSync(path.join(source, 'app'));
fs.writeFileSync(
path.join(source, 'app/page.jsx'),
"import App from '../main.jsx';export default function Page(){return <App/>}\n",
);
fs.writeFileSync(
path.join(source, 'app/layout.jsx'),
'export default function Layout({children}){return <html lang="en"><body>{children}</body></html>}\n',
);
fs.writeFileSync(
path.join(consumer, 'next.config.mjs'),
"import {withDomscribe} from '@domscribe/next';export default withDomscribe({overlay:true,relay:{port:0}})({reactStrictMode:true,experimental:{cpus:2}});\n",
);
} else if (appFramework === 'nuxt') {
fs.rmSync(path.join(consumer, 'index.html'));
fs.rmSync(path.join(consumer, 'vite.config.mjs'));
fs.rmSync(path.join(source, 'main.js'));
fs.renameSync(path.join(source, 'App.vue'), path.join(source, 'app.vue'));
fs.mkdirSync(path.join(source, 'plugins'));
fs.writeFileSync(
path.join(source, 'plugins/evidence.client.js'),
"export default defineNuxtPlugin(async()=>{if(import.meta.dev){const {RuntimeManager}=await import('@domscribe/runtime');window.__runtime=RuntimeManager.getInstance();}});\n",
);
fs.writeFileSync(
path.join(consumer, 'nuxt.config.mjs'),
"export default defineNuxtConfig({srcDir:'src/',compatibilityDate:'2026-10-25',devtools:{enabled:false},modules:['@domscribe/nuxt'],domscribe:{overlay:true,relay:{port:0}},app:{head:{title:'Instance replay regression'}}});\n",
);
}
}
Loading
Loading