diff --git a/.claude/skills/router-design/SKILL.md b/.claude/skills/router-design/SKILL.md new file mode 100644 index 0000000000..d19a53148b --- /dev/null +++ b/.claude/skills/router-design/SKILL.md @@ -0,0 +1,106 @@ +--- +name: router-design +description: 'Stage 2: Analyze reference implementations and produce design decisions from the stage 1 JSON. Reads 01-router-concepts.json and reference code, emits JSON conforming to output.schema.json.' +--- + +# Stage 2: Design Decisions + +## Context + +You are Stage 2 of the router integration pipeline. Your job is to read the structured router concepts from Stage 1, analyze the existing reference implementations, and produce explicit design decisions that will guide code generation in Stage 3. + +The pipeline invokes you with `claude -p --output-format json --json-schema .claude/skills/router-design/output.schema.json`. Your final message must be a single JSON object conforming to that schema; the harness validates it and writes the CLI wrapper to `docs/integrations//02-design-decisions.json` with the payload on `.structured_output`. + +## Input + +You receive a **framework identifier** as skill param (e.g. `angular`, `vue`, `tanstack-react-router`). + +Read: + +1. `docs/integrations//01-router-concepts.json` — stage 1 CLI wrapper. Extract the router-concepts payload with `jq '.structured_output' `. +2. Reference implementations (to understand SDK patterns): + - Plugin files: `packages/rum-vue/src/domain/vuePlugin.ts`, `packages/rum-react/src/domain/reactPlugin.ts`, `packages/rum-nextjs/src/domain/nextjsPlugin.ts` + - Vue router: `packages/rum-vue/src/domain/router/` (all `.ts` files) + - React router: `packages/rum-react/src/domain/reactRouter/` (all `.ts` files) + - Next.js router: `packages/rum-nextjs/src/domain/nextJSRouter/` (all `.ts` files) + - Entry points: `packages/rum-vue/src/entries/main.ts`, `packages/rum-react/src/entries/main.ts`, `packages/rum-nextjs/src/entries/main.ts` + - Package configs: `packages/rum-vue/package.json`, `packages/rum-react/package.json`, `packages/rum-nextjs/package.json` + - Plugin interface: `packages/rum-core/src/domain/plugins.ts` + +## Process + +### 1. Hook Selection (Deterministic) + +Apply these priority rules to the `hooks` array from `01-router-concepts.json`. + +**The integration must be client-side only.** Only consider hooks that fire on the client. Use the `access` field and `ssr` section from `01-router-concepts.json` to determine this. + +**Priority rules (in order):** + +1. `afterCancellation: true` — **required**. Never start a RUM view for a navigation that didn't occur. +2. `afterRedirects: true` — **prefer**. Report the final destination, not intermediate routes. +3. `afterFetch: false` AND `afterRender: false` — **prefer**. Start the view before data loading and DOM mutation so RUM events (fetch resources, long tasks, interactions) are attributed to the new view, not the previous one. + +Apply in order: + +- Filter to `afterCancellation: true`. If no hooks pass, flag as critical issue and stop. +- Among those, prefer `afterRedirects: true`. +- Among those, prefer `afterFetch: false` AND `afterRender: false`. +- If rules conflict (no hook satisfies all), higher-priority rule wins. +- If multiple hooks still tie, prefer the one that fires earliest in the lifecycle. + +Document which hooks were considered, which rules each passed/failed, and why the selected hook won. + +### 2. Wrapping Strategy (LLM Judgment) + +Read the selected hook's `access` field from `01-router-concepts.json`. Determine the most idiomatic way for users to integrate the plugin in this framework. + +Consider: + +- How existing plugins/libraries are typically added in this framework's ecosystem +- Whether the hook needs a router instance (→ wrap the factory that creates it) +- Whether the hook needs component context (→ renderless component or hook) +- Whether the hook needs DI (→ provider registration) + +Reference patterns from existing implementations: + +- Vue: wraps `createRouter()` factory to get router instance for `afterEach` +- React: wraps `createBrowserRouter()` factory OR wraps `useRoutes()` hook +- Angular: provider with `inject(Router)` for `router.events` observable + +### 3. View Name Algorithm (LLM Classification) + +Read the selected hook's `availableApi` from `01-router-concepts.json`. Classify into one of three families (in preference order): + +- **`route-id`** — Framework provides the parameterized route pattern as a string. Minimal post-processing needed (e.g. strip route groups). Example: SvelteKit `route.id`. +- **`matched-records`** — Framework provides matched route records (array or tree). Iterate and concatenate path segments. Handle catch-all substitution. Example: Vue `to.matched[]`, React `state.matches[]`. +- **`param-substitution`** — Framework provides only the evaluated pathname + params object. Must reconstruct the route template by substituting values back with placeholders. Least preferred — heuristic and fragile. Example: Next.js `useParams()` + `usePathname()`. + +### 4. Target Package (LLM Judgment) + +Determine whether this router needs a new package or extends an existing one. + +- **`new-package`** — The router belongs to a framework with no existing SDK package (e.g., SvelteKit, Angular). Create `packages/rum-/`. +- **`extend-existing`** — The router is an alternative router for a framework that already has an SDK package (e.g., TanStack Router is a React router → extends `rum-react`). Add files under a subdirectory within the existing package. + +To decide: check if `packages/rum-*` already has a package for the same UI framework (React, Vue, etc.). If yes, extend it. If no, create new. + +For extend-existing, also determine the subdirectory path for the new router files (e.g., `src/domain/tanstackRouter/`). + +### 5. Reference Implementation + +Select the `packages/rum-*` implementation that is closest across: + +- Hook subscription pattern +- Wrapping strategy +- Algorithm family + +Stage 3 reads this implementation as its primary model for code generation. + +### 6. SSR Handling (LLM Judgment) + +If `ssr.supported: true` in `01-router-concepts.json`, describe how the integration should ensure client-side-only execution. Use the `clientDetection` API from Stage 1 if available. + +## Output Schema + +Return the populated object as your final message. The pipeline invokes you with `--output-format json --json-schema output.schema.json`; the harness validates the object and writes the full CLI wrapper (with the object on `.structured_output`) to `docs/integrations//02-design-decisions.json`. Do not write files yourself. diff --git a/.claude/skills/router-design/output.schema.json b/.claude/skills/router-design/output.schema.json new file mode 100644 index 0000000000..8344a426d1 --- /dev/null +++ b/.claude/skills/router-design/output.schema.json @@ -0,0 +1,118 @@ +{ + "title": "DesignDecisions", + "description": "Explicit design decisions derived from Stage 1 router concepts and reference implementations, used by Stage 3 to generate code.", + "type": "object", + "additionalProperties": false, + "required": [ + "selectedHook", + "wrappingStrategy", + "viewNameAlgorithm", + "targetPackage", + "referenceImplementation", + "ssr" + ], + "properties": { + "selectedHook": { + "type": "object", + "additionalProperties": false, + "required": ["name", "rationale"], + "properties": { + "name": { + "type": "string", + "description": "Hook name from 01-router-concepts.json, selected by the deterministic priority rules." + }, + "rationale": { + "type": "string", + "description": "Which rules each candidate passed/failed and why the selected hook won." + } + } + }, + "wrappingStrategy": { + "type": "object", + "additionalProperties": false, + "required": ["pattern", "target", "rationale"], + "properties": { + "pattern": { + "enum": ["wrap-factory", "renderless-component", "provider", "wrap-hook", "other"], + "description": "wrap-factory: Wrap the router creation function, subscribe to hook inside. renderless-component: Component that calls the hook during lifecycle. provider: DI provider that injects the router and subscribes to events. wrap-hook: Wrap a user-facing hook to intercept route data. other: Escape hatch for unknown patterns." + }, + "target": { + "type": "string", + "description": "What specifically to wrap/provide. E.g. 'createRouter from vue-router', 'ENVIRONMENT_INITIALIZER with inject(Router)'." + }, + "rationale": { + "type": "string", + "description": "Why this is idiomatic for the framework." + } + } + }, + "viewNameAlgorithm": { + "type": "object", + "additionalProperties": false, + "required": ["family", "rationale"], + "properties": { + "family": { + "enum": ["route-id", "matched-records", "param-substitution"], + "description": "route-id: Framework provides parameterized route pattern as string. Minimal post-processing. matched-records: Framework provides matched route records. Iterate and concatenate path segments. param-substitution: Framework provides evaluated pathname + params. Reconstruct route template. Least preferred." + }, + "rationale": { + "type": "string", + "description": "Why this family, based on the hook's availableApi." + } + } + }, + "targetPackage": { + "type": "object", + "additionalProperties": false, + "required": ["mode", "package"], + "properties": { + "mode": { + "enum": ["new-package", "extend-existing"], + "description": "new-package: Create a new packages/rum-/ from scratch. extend-existing: Add router support to an existing package (e.g. adding TanStack Router to rum-react)." + }, + "package": { + "type": "string", + "description": "Target package directory name. For new-package: 'rum-'. For extend-existing: the existing package (e.g. 'rum-react')." + }, + "subpath": { + "type": "string", + "description": "Only for extend-existing. The subdirectory for this router's files within the existing package. E.g. 'src/domain/tanstackRouter/' within packages/rum-react/." + } + } + }, + "referenceImplementation": { + "type": "object", + "additionalProperties": false, + "required": ["primary", "rationale"], + "properties": { + "primary": { + "type": "string", + "description": "Which packages/rum-* to model after (e.g. 'rum-vue'). Stage 3 reads this implementation as its primary source for code patterns." + }, + "rationale": { + "type": "string", + "description": "Why this is the closest match." + } + } + }, + "ssr": { + "type": "object", + "additionalProperties": false, + "required": ["handling"], + "properties": { + "handling": { + "type": "string", + "description": "How to ensure client-side-only execution. 'N/A' if ssr.supported is false in Stage 1." + } + } + }, + "notes": { + "type": "string", + "description": "Free text for additional design context, trade-offs, unmapped concepts, or anything Stage 3 needs to know." + }, + "exitReason": { + "type": "string", + "description": "Only set if Stage 2 cannot proceed (e.g., no hook satisfies afterCancellation: true). When set, all other fields may be empty stubs and the pipeline will stop." + } + } +} diff --git a/.claude/skills/router-fetch-docs/SKILL.md b/.claude/skills/router-fetch-docs/SKILL.md new file mode 100644 index 0000000000..7a1a11c44f --- /dev/null +++ b/.claude/skills/router-fetch-docs/SKILL.md @@ -0,0 +1,85 @@ +--- +name: router-fetch-docs +description: 'Stage 1: Fetch framework router documentation and extract structured routing concepts as JSON conforming to output.schema.json from an npm package URL.' +--- + +# Stage 1: Fetch Router Documentation + +## Context + +You are Stage 1 of the router integration pipeline. Your job is to resolve package metadata, fetch framework router documentation, and extract structured routing concepts as **factual data** for later stages. + +The pipeline invokes you with `claude -p --output-format json --json-schema .claude/skills/router-fetch-docs/output.schema.json`. Your final message must be a single JSON object conforming to that schema; the harness validates it and exposes it on `.structured_output` of the CLI wrapper written to `docs/integrations//01-router-concepts.json`. + +Do NOT analyze which hooks the SDK should use, recommend approaches, or compare with existing SDK integrations. Only document what the framework provides. + +## Input + +You receive an **npm package URL** as skill param (e.g. `https://www.npmjs.com/package/vue-router`). + +## Process + +### 1. Resolve Package Metadata + +Use WebFetch on the provided npm URL to extract: + +- **Package name** (e.g. `vue-router`, `@angular/router`) +- **Framework identifier** — derive a lowercase identifier from the package name (e.g. `vue-router` → `vue`, `@angular/router` → `angular`, `@tanstack/react-router` → `tanstack-react-router`) +- Homepage / repository URL +- Keywords and description + +Then **find router documentation URLs** — from the npm page metadata (homepage, repository links), locate the framework's official routing documentation: + +- Check the homepage URL for docs links +- Check the GitHub repository README for documentation links +- Look for `/docs/`, `/guide/`, `/routing` paths on the framework's site +- Collect 1-3 relevant documentation URLs focused on routing + +The pipeline creates the artifact directory for you — do not run `mkdir` yourself. + +### 2. Fetch Documentation + +Before fetching the provided URLs directly, try to find LLM-friendly versions of the docs: + +1. **Check for `llms.txt`** — Fetch `/llms.txt` (e.g. `https://svelte.dev/llms.txt`). This file indexes markdown documentation pages designed for LLM consumption. If it exists, use it to navigate to the relevant routing pages. +2. **Try `.md` suffix** — For each doc URL, try appending `.md` to the path (e.g. `https://svelte.dev/docs/kit/routing.md`). Many doc sites serve a raw markdown version this way, which is much easier to parse accurately. +3. **Fall back to HTML** — If neither LLM-friendly format is available, fetch the original URLs. + +Use the WebFetch tool for all fetches. If a URL fails, note it and continue with remaining URLs. If all URLs fail, stop without emitting a JSON object — the harness will mark the run as an error. + +**Prefer over-fetching to under-fetching.** Fetch every routing-related page you can find — API references, guides, tutorials. It is better to fetch a page and not need it than to miss information that a later stage requires. When in doubt, fetch it. + +### 3. Extract Router Concepts into the JSON Schema + +Analyze the fetched documentation and populate every field in the JSON schema. Use `null` for features the framework does not support. + +#### Sourcing Rules + +Every leaf field has a sibling `source` field. This is **mandatory** — the schema validation fails without it. + +- If extracted from documentation: set `source` to the URL (with anchor if possible) +- If inferred from multiple sources or reasoning: set `source` to `"inferred: "` +- API names, hook names, type names — everything factual must be traceable to a specific doc page + +#### JSON Schema + +Read `output.schema.json` (next to this SKILL.md) for the schema and field descriptions. + +### 4. Compatibility Assessment + +A framework is **incompatible** if it lacks: + +- A client-side route tree or route matching mechanism +- Dynamic segment parameters +- Navigation lifecycle events that can be hooked into + +If incompatible: stop without emitting a JSON object. The harness will mark the run as an error. + +A framework is **compatible** even if: + +- It uses file-based routing (as long as there's a runtime route representation) +- Some hooks fire later than ideal (trade-off is documented, not a blocker) + +## Output + +Return the populated object as your final message. The pipeline invokes you with `--output-format json --json-schema output.schema.json`; the harness validates the object and writes the full CLI wrapper (with the object on `.structured_output`) to `docs/integrations//01-router-concepts.json`. Do not write files yourself. diff --git a/.claude/skills/router-fetch-docs/output.schema.json b/.claude/skills/router-fetch-docs/output.schema.json new file mode 100644 index 0000000000..6f1dd453e1 --- /dev/null +++ b/.claude/skills/router-fetch-docs/output.schema.json @@ -0,0 +1,235 @@ +{ + "title": "RouterConcepts", + "description": "Structured routing concepts extracted from framework documentation. Every factual leaf has a sibling `source` field: a URL (with anchor if possible) or 'inferred: '.", + "type": "object", + "additionalProperties": false, + "required": ["metadata", "routeDefinition", "routeSyntax", "ssr", "hooks", "versions"], + "properties": { + "metadata": { + "type": "object", + "additionalProperties": false, + "required": ["framework", "npmPackage"], + "properties": { + "framework": { + "type": "object", + "additionalProperties": false, + "required": ["value", "source"], + "properties": { + "value": { + "type": "string", + "description": "Lowercase identifier used for directory and package names. Derived from npm package name (e.g. vue-router → vue, @tanstack/react-router → tanstack-react-router)." + }, + "source": { "type": "string" } + } + }, + "npmPackage": { + "type": "object", + "additionalProperties": false, + "required": ["value", "source"], + "properties": { + "value": { + "type": "string", + "description": "npm package name (e.g. vue-router, @angular/router)." + }, + "source": { "type": "string" } + } + } + } + }, + "routeDefinition": { + "type": "object", + "additionalProperties": false, + "required": ["styles", "historyStrategies"], + "properties": { + "styles": { + "type": "array", + "description": "How routes are declared. file-based = filesystem convention (SvelteKit, Next.js). config-object = explicit route configuration array (Vue, React, Angular). other = escape hatch. Array because a framework may support multiple styles.", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["value", "source"], + "properties": { + "value": { "enum": ["file-based", "config-object", "other"] }, + "source": { "type": "string" } + } + } + }, + "historyStrategies": { + "type": "array", + "description": "URL strategies the router supports. path = browser History API (pushState). hash = hash-based (#/route). memory = in-memory (SSR, testing).", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["value", "source"], + "properties": { + "value": { "enum": ["path", "hash", "memory"] }, + "source": { "type": "string" } + } + } + } + } + }, + "routeSyntax": { + "type": "object", + "additionalProperties": false, + "required": ["concepts", "examples"], + "properties": { + "concepts": { + "type": "array", + "description": "Which routing concepts the framework supports. Use this enum to flag concepts so later stages never forget to handle them.", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["value", "source"], + "properties": { + "value": { + "enum": ["static-path", "dynamic-segments", "optional-segments", "catch-all", "nested-routes"] + }, + "source": { "type": "string" } + } + } + }, + "examples": { + "type": "array", + "description": "Behavioral examples showing route pattern, actual URL, and expected view name. Must cover every concept listed above. Include framework-specific quirks.", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["route", "path", "viewName", "source"], + "properties": { + "route": { "type": "string", "description": "Route pattern as declared." }, + "path": { "type": "string", "description": "Example URL that matches." }, + "viewName": { "type": "string", "description": "Expected view name (pattern with placeholders)." }, + "note": { "type": "string", "description": "Explain non-obvious behavior." }, + "source": { "type": "string" } + } + } + } + } + }, + "ssr": { + "type": "object", + "additionalProperties": false, + "required": ["supported", "clientDetection"], + "properties": { + "supported": { + "type": "object", + "additionalProperties": false, + "required": ["value", "source"], + "properties": { + "value": { "type": "boolean" }, + "source": { "type": "string" } + } + }, + "clientDetection": { + "type": "object", + "additionalProperties": false, + "required": ["value", "source"], + "properties": { + "value": { + "type": ["string", "null"], + "description": "Framework's idiomatic API for detecting client-side execution. Used by Stage 3 to generate guards when needed." + }, + "source": { "type": "string" } + } + } + } + }, + "hooks": { + "type": "array", + "description": "ALL candidate navigation lifecycle hooks (client-side).", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "afterCancellation", + "afterRedirects", + "afterFetch", + "afterRender", + "access", + "availableApi", + "source" + ], + "properties": { + "name": { + "type": "string", + "description": "Hook/event name as it appears in the framework API." + }, + "afterCancellation": { + "type": "boolean", + "description": "At this hook, is the navigation guaranteed to have survived all cancellation points? Guard redirects (e.g. Angular returning a UrlTree) cancel the current navigation and start a new one — the hook fires only for the final, non-cancelled navigation." + }, + "afterRedirects": { + "type": "boolean", + "description": "At this hook, is the observed route guaranteed to be the final destination? Config redirects resolved before this hook, and programmatic redirects that cancel+restart navigation, both count as 'after' if the cancelled navigation never reaches this hook." + }, + "afterFetch": { + "type": "boolean", + "description": "Fires after data loading (resolvers, load functions)." + }, + "afterRender": { + "type": "boolean", + "description": "Fires after DOM mutation / component rendering." + }, + "access": { + "type": "string", + "description": "What runtime context is needed to call this hook. Examples: 'Return value of createRouter()', 'Must be called during Svelte component init', 'Router service injected via Angular DI'." + }, + "availableApi": { + "type": "array", + "description": "Data accessible at hook invocation time.", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["name", "description", "source"], + "properties": { + "name": { "type": "string", "description": "API path (e.g. to.matched, navigation.to.route.id)." }, + "description": { "type": "string" }, + "source": { "type": "string" } + } + } + }, + "source": { + "type": "string", + "description": "URL of the hook's API reference (or 'inferred: ')." + } + } + } + }, + "versions": { + "type": "object", + "additionalProperties": false, + "required": ["entries"], + "properties": { + "entries": { + "type": "array", + "description": "Routing-relevant breaking changes only, max 2 years old.", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["version", "date", "routingChanges", "source"], + "properties": { + "version": { "type": "string", "description": "Semver version string." }, + "date": { "type": "string", "description": "Release date (YYYY-MM-DD)." }, + "routingChanges": { + "type": "array", + "description": "Breaking changes affecting routing.", + "items": { "type": "string" } + }, + "source": { "type": "string", "description": "URL of changelog or release notes." } + } + } + } + } + }, + "notes": { + "type": "string", + "description": "Free text for framework-specific context that doesn't fit the schema above." + }, + "incompatibilityReason": { + "type": "string", + "description": "Only set if the framework is incompatible (no client-side route tree, no dynamic segments, or no navigation lifecycle hooks). When set, all other fields may be empty stubs and the pipeline will stop." + } + } +} diff --git a/.claude/skills/router-generate/SKILL.md b/.claude/skills/router-generate/SKILL.md new file mode 100644 index 0000000000..5d7fcf927f --- /dev/null +++ b/.claude/skills/router-generate/SKILL.md @@ -0,0 +1,184 @@ +--- +name: router-generate +description: 'Stage 3: Generate the complete router integration package from design artifacts and reference implementations.' +--- + +# Stage 3: Generate Package + +## Context + +You are Stage 3 of the router integration pipeline. Your job is to generate a complete, working Browser SDK router integration package based on the design decisions from Stage 2 and the patterns from reference implementations. + +## Input + +You receive a **framework identifier** as skill param (e.g. `angular`, `vue`, `tanstack-react-router`). + +Read: + +1. `docs/integrations//01-router-concepts.json` — stage 1 CLI wrapper. Extract the router-concepts payload with `jq '.structured_output' `. +2. `docs/integrations//02-design-decisions.json` — stage 2 CLI wrapper. Extract the design payload with `jq '.structured_output' `. +3. `.claude/skills/router-fetch-docs/output.schema.json` — field definitions for stage 1 output. +4. `.claude/skills/router-design/output.schema.json` — field definitions for stage 2 output. +5. Reference implementation specified by `referenceImplementation.primary` in the stage 2 payload. Read the source files in `packages//`: + - Plugin file: `src/domain/*Plugin.ts` + - Router files: `src/domain/*Router/` or `src/domain/router/` (all `.ts` files) + - Tests: corresponding `.spec.ts` files + - Test helper: `src/test/initialize*Plugin.ts` + - Package config: `package.json`, `tsconfig.json` + - Entry point: `src/entries/main.ts` + +## Process + +Generate files in two phases. Complete each phase before starting the next. + +--- + +### Phase 1: Plugin + Wrapping Strategy + +Generate the plugin file and the framework-specific integration point. This phase reads `targetPackage`, `wrappingStrategy`, and `selectedHook` from `02-design-decisions.json`. + +**Check `targetPackage.mode` first:** + +- **`new-package`**: Create the full package scaffolding (`package.json`, `tsconfig.json`, `README.md`, `src/entries/main.ts`, `src/test/initializePlugin.ts`) by reading from the reference implementation. Then generate plugin + integration files. +- **`extend-existing`**: The package already exists at `packages//`. Do NOT create package scaffolding. Add router files under `targetPackage.subpath` (e.g., `src/domain/tanstackRouter/`). Update `src/entries/main.ts` to add new exports. Update `package.json` to add new peer dependencies if needed. The existing plugin file may need a new configuration option or the new router may reuse the existing plugin's `onRumInit`/`onRumStart` subscribers. + +**Files to generate:** + +For `new-package`: + +- `src/domain/Plugin.ts` — plugin lifecycle, subscriber pattern, configuration +- Integration point file — hook subscription +- `src/domain/Plugin.spec.ts` — plugin structure tests + +For `extend-existing`: + +- Integration point file under `targetPackage.subpath` — hook subscription +- View name and types files under `targetPackage.subpath` +- Tests under `targetPackage.subpath` +- Modify `src/entries/main.ts` — add exports for the new router +- Modify `package.json` — add peer dependencies if needed + +**Plugin file** (only for `new-package`): + +- Follow the exact pattern from the reference: global state, subscriber arrays, `onRumInit`/`onRumStart` exports, `resetPlugin` for tests +- Plugin name must be the framework name in lowercase +- Configuration interface with `router?: boolean` +- `getConfigurationTelemetry` returning `{ router: !!configuration.router }` + +**Integration point file:** + +The wrapping strategy from `02-design-decisions.json` determines what this file looks like: + +- `wrap-factory` → wrap the framework's router creation function, subscribe to hook inside +- `renderless-component` → Svelte/React component that calls the hook during lifecycle +- `provider` → DI provider that injects the router and subscribes to events +- `wrap-hook` → wrap a user-facing hook to intercept route data + +Read the selected hook's `access` and `availableApi` from `01-router-concepts.json` to understand what data is available and how to reach it. + +If `ssr.handling` is not "N/A", add the client-side guard described there. + +--- + +### Phase 2: View Name Algorithm + +Generate the view name computation, types, and tests. This phase reads `viewNameAlgorithm` and `routeSyntax` from the design artifacts. + +**Files to generate:** + +- `src/domain/Router/startView.ts` — `computeViewName()` + `startRouterView()`. Driven by `viewNameAlgorithm.family`. +- `src/domain/Router/types.ts` — minimal local types for route data. Driven by `selectedHook.availableApi`. +- `src/domain/Router/startView.spec.ts` — view name computation tests. Driven by `routeSyntax.examples`. + +**`computeViewName()` implementation by family:** + +- **`route-id`**: Framework provides route pattern as string. Implementation is minimal — mostly cleanup (e.g., strip route groups). Model after the simplest reference. +- **`matched-records`**: Iterate matched route records, concatenate `.path` fields. Handle absolute vs relative paths. Substitute catch-all patterns with actual path segments. Model after `packages/rum-vue/src/domain/router/startVueRouterView.ts` or `packages/rum-react/src/domain/reactRouter/startReactRouterView.ts`. +- **`param-substitution`**: Reverse-engineer route template from pathname + params. Two-pass: catch-all arrays first, then string params. Model after `packages/rum-nextjs/src/domain/nextJSRouter/computeViewNameFromParams.ts`. + +**Test cases:** + +Read the reference implementation's test files first. Match the same coverage and writing style: + +- Same `describe`/`it` structure and naming conventions +- Same test case table format (e.g., `const cases = [...]` with inline comments) +- Same helper patterns (mock setup, cleanup) +- Same level of edge case coverage + +Then adapt test cases for the target framework: + +- Start from `routeSyntax.examples` in `01-router-concepts.json` — each example maps to a test case +- Add edge cases by looking at what the reference test covers and translating to the target framework's syntax +- Every `routeSyntax.concept` must have at least one test case + +**Types file:** + +- Define minimal interfaces matching what the selected hook's `availableApi` exposes +- Avoid runtime framework imports — use local type definitions +- Only type what `computeViewName()` and the integration point actually need + +--- + +### Phase 3: Validate & Fix + +Run the monorepo toolchain against the generated code. Fix failures in a bounded loop. Only modify files you generated in Phase 1 and Phase 2 — never touch reference implementations, core packages, or config files outside the generated package. + +**1. Register the package in the workspace:** + +```bash +yarn +``` + +For `new-package` mode, verify it appears in `yarn workspaces list`. + +**2. Run all checks, fix, repeat — up to 5 iterations:** + +``` +checks = [typecheck, lint, unit tests] + +for iteration in 1..5: + run `yarn format` + run `yarn typecheck` → capture errors + run `yarn lint` → capture errors + run `yarn test:unit --spec packages/rum-/` → capture errors + + if all pass → done, break + + analyze ALL errors together + apply fixes to generated files only + log what was fixed and why + +if iteration 5 still has failures → stop, document in manifest +``` + +**Checks (run all three every iteration):** + +| Command | What it catches | +| ------------------------------------------------- | -------------------------------------------------------- | +| `yarn typecheck` | Wrong imports, missing properties, mismatched interfaces | +| `yarn lint` | Naming, formatting (mostly auto-fixed by `yarn format`) | +| `yarn test:unit --spec packages/rum-/` | Wrong mock shapes, assertion mismatches, missing cleanup | + +Running all three every iteration (instead of gating sequentially) lets you see the full error surface and fix multiple categories at once. + +**Fix strategy:** + +- Read ALL errors before making any changes — fixes often overlap +- Compare with the reference implementation's source and test files to spot pattern mismatches +- Prefer the smallest change that fixes the error + +**Anti-rules — never do these to "fix" a failure:** + +- Add `// @ts-ignore`, `as any`, or type casts to silence type errors +- Delete or skip failing test cases + +## After All Phases: Write Generation Manifest + +Write `docs/integrations//03-generation-manifest.md` using the template at `output.template.md` (next to this SKILL.md). Fill in every placeholder. The validation section documents all fix loop iterations. + +If validation status is `fail`, the pipeline orchestrator must stop before creating a PR. + +## Output + +- Generated package in `packages/rum-/` +- Manifest at `docs/integrations//03-generation-manifest.md` diff --git a/.claude/skills/router-generate/output.template.md b/.claude/skills/router-generate/output.template.md new file mode 100644 index 0000000000..b9a607787b --- /dev/null +++ b/.claude/skills/router-generate/output.template.md @@ -0,0 +1,27 @@ +# Generation Manifest — + +## Generated Files + +### Phase 1: Plugin + Wrapping Strategy + +- `` — + - Reference: `` + - Deviations: + +### Phase 2: View Name Algorithm + +- `` — + - Reference: `` + - Deviations: + +## Validation + +**Status:** pass | fail +**Iterations:** / 5 + +### Iteration , , + +- + + + diff --git a/.claude/skills/router-pipeline/README.md b/.claude/skills/router-pipeline/README.md new file mode 100644 index 0000000000..c2f94aa765 --- /dev/null +++ b/.claude/skills/router-pipeline/README.md @@ -0,0 +1,81 @@ +# Router Integration Pipeline + +**One command in, draft PR out.** + +Takes an npm URL like `vue-router`, produces a full Browser SDK router integration. + +--- + +## Context + +- Many integration we build so far follow the same pattern (React, Angular, ...) +- Repetitive, pattern-heavy work → good automation candidate +- Scoped to **one feature** (route-pattern view naming) to stay practical and shippable +- **What's a router integration?** RUM tracks views (one per navigation). Default view name = raw URL, so `/users/42` and `/users/1337` look like different pages. Customers' routers use **patterns** (`/users/:id`) — integration hooks the router so the view name is the _pattern_, aggregating cleanly in dashboards. + +--- + +## The Flow + +```mermaid +flowchart LR + U([npm URL]) --> S1[Fetch Docs] --> A1([concepts.json]) + A1 --> S2[Design] --> A2([decisions.json]) + A2 --> S3[Generate] --> A3([code + tests]) + A3 --> S4[PR] --> PR([draft PR]) + + classDef step fill:#dbeafe,stroke:#1d4ed8,stroke-width:2px,color:#000 + classDef output fill:#fef3c7,stroke:#b45309,stroke-dasharray:4 2,color:#000 + class S1,S2,S3,S4 step + class U,A1,A2,A3,PR output +``` + +4 stages, each a separate `claude -p` process: + +1. **Fetch docs** → schema-validated JSON of routing concepts +2. **Design** → schema-validated design decisions +3. **Generate** → TypeScript code + tests, self-validates +4. **PR** → commits, pushes, opens draft + +--- + +## AI Guidelines That Made It Work + +### Isolated context per stage + +Fresh `claude -p` each step. No context bloat, no cross-contamination, cheaper tokens. + +### Structured data between stages + +`--json-schema` enforced by the harness. Downstream stages `jq` into guaranteed shapes instead of parsing prose. Hallucinations caught at the boundary. + +### Reference implementations as ground truth + +Stage 2 reads existing React/Angular integrations before designing. The model pattern-matches against _working code_, not its own priors. + +### Harness-enforced validation, not vibes + +Schema validation on JSON stages, typecheck/lint gate on codegen. Pipeline _stops_ on failure rather than producing plausible-looking garbage. + +### Persisted artifacts at every boundary + +`docs/integrations//` holds each stage's output. Reviewable, resumable, debuggable. You can rerun stage 3 without redoing 1-2. + +### Narrow, composable skills + +Each stage is its own slash command. Testable in isolation, orchestrator just sequences them. + +--- + +## Net + +**Structured hand-offs between focused agents, grounded in real code, validated by the harness.** + +--- + +## Lessons Learned + +- **Many ways to orchestrate agents** — skills, subagents, `claude -p` CLI, raw API. Each has different trade-offs (context isolation, composability, cost, observability). Pick deliberately per stage. +- **Read the actual Claude docs** — I tried asking Claude about its own best practices and got vague/inconclusive answers. Docs are the source of truth. +- **Building this takes time** — each stage has to be developed in isolation, iteration is slow, lots of manual review of intermediate outputs early on. The upfront cost is real. +- **Next: self-prompt improvement** — want to experiment with letting the pipeline refine its own prompts from review feedback, instead of me tuning them by hand. diff --git a/.claude/skills/router-pipeline/SKILL.md b/.claude/skills/router-pipeline/SKILL.md new file mode 100644 index 0000000000..e44c80426c --- /dev/null +++ b/.claude/skills/router-pipeline/SKILL.md @@ -0,0 +1,78 @@ +--- +name: router-pipeline +description: Fully automated pipeline that generates a draft Browser SDK router integration PR from an npm package URL. Usage: /router-pipeline +--- + +# Router Integration Pipeline + +You are an orchestrator that dispatches `claude -p` processes for each stage to generate a complete Browser SDK router integration package and draft PR. + +Each stage runs as a dedicated `claude -p` process with its own context window. Stages 1 and 2 use `--output-format json --json-schema `. The full CLI wrapper JSON (including `structured_output`, `is_error`, `duration_ms`, cost, etc.) is persisted as the reviewable artifact under `docs/integrations//`. Downstream stages extract `.structured_output` with `jq` when they read the data. Stages 3 and 4 produce code and markdown via normal tool calls. + +## Input + +The single argument is an npm package URL (e.g. `https://www.npmjs.com/package/@angular/router`). + +Example: `/router-pipeline https://www.npmjs.com/package/vue-router` + +## Step 1: Stage 1 — Fetch Docs (structured JSON) + +The skill instructs the model to emit a JSON object conforming to `.claude/skills/router-fetch-docs/output.schema.json`. The harness validates it and exposes it on `.structured_output`. + +The framework name isn't known yet, so write the artifact to a real temp file first, then move it under `docs/integrations//` once stage 1 resolves it. + +```bash +SCHEMA_1=$(cat .claude/skills/router-fetch-docs/output.schema.json) + +STAGE1_OUT=/tmp/router-stage1.json + +claude -p "/router-fetch-docs " \ + --model opus \ + --output-format json \ + --json-schema "$SCHEMA_1" \ + --permission-mode auto \ + > "$STAGE1_OUT" + +FRAMEWORK=$(jq -r '.structured_output.metadata.framework.value' "$STAGE1_OUT") +mkdir -p "docs/integrations/$FRAMEWORK" +mv "$STAGE1_OUT" "docs/integrations/$FRAMEWORK/01-router-concepts.json" +``` + +The file committed under `docs/integrations/$FRAMEWORK/01-router-concepts.json` is the full CLI wrapper: `{type, subtype, is_error, duration_ms, result, structured_output, usage, total_cost_usd, ...}`. Downstream stages use `jq '.structured_output' ` to read the schema-shaped payload. + +## Step 2: Stage 2 — Design Decisions (structured JSON) + +```bash +SCHEMA_2=$(cat .claude/skills/router-design/output.schema.json) + +claude -p "/router-design $FRAMEWORK" \ + --model opus \ + --output-format json \ + --json-schema "$SCHEMA_2" \ + --permission-mode auto \ + > "docs/integrations/$FRAMEWORK/02-design-decisions.json" +``` + +## Step 3: Stage 3 — Generate Code + +Stage 3 produces source code and a markdown manifest via normal tool calls — no structured output. + +```bash +claude -p "/router-generate $FRAMEWORK" \ + --model opus \ + --permission-mode auto +``` + +Verify `docs/integrations/$FRAMEWORK/03-generation-manifest.md` + +Check the **Validation** section in the manifest. If `**Status:** fail`, stop the pipeline and report the failures to the user. Do not proceed to Stage 4. + +## Step 4: Stage 4 — Create PR + +```bash +claude -p "/router-pr $FRAMEWORK" \ + --model opus \ + --permission-mode auto +``` + +Report the PR URL to the user when done. diff --git a/.claude/skills/router-pr/SKILL.md b/.claude/skills/router-pr/SKILL.md new file mode 100644 index 0000000000..8bd597bc57 --- /dev/null +++ b/.claude/skills/router-pr/SKILL.md @@ -0,0 +1,116 @@ +--- +name: router-pr +description: 'Stage 4: Create a git branch, commit artifacts and generated code, and open a draft PR on GitHub.' +--- + +# Stage 4: Create Draft PR + +## Context + +You are Stage 4 of the router integration pipeline. Your job is to create a branch, commit all generated artifacts and code, and open a draft PR on GitHub. + +## Input + +You receive a **framework identifier** as skill param (e.g. `angular`, `vue`, `tanstack-react-router`). + +Read: + +1. `docs/integrations//02-design-decisions.json` — stage 2 CLI wrapper. Extract the design payload (used for PR body content) with `jq '.structured_output' `. +2. `docs/integrations//03-generation-manifest.md` — list of generated files. +3. `git config user.name` — current git user name (for branch naming). + +## Process + +### 1. Create Branch + +```bash +# Get the current user's branch prefix +USER=$(git config user.name | tr ' ' '.' | tr '[:upper:]' '[:lower:]') +# Random suffix to avoid branch collisions across repeated runs +BRANCH="${USER}/-router-integration-${RANDOM}" + +git checkout -b "$BRANCH" +``` + +### 2. Commit Artifacts + +First commit: design documentation only. + +```bash +git add docs/integrations// +git commit -m "📝 Add router integration design docs + +Generated by the router integration pipeline. +Artifacts: router concepts, design decisions, generation manifest." +``` + +### 3. Commit Generated Code + +Second commit: the generated code. Read `targetPackage.mode` from `02-design-decisions.json`. + +For `new-package`: + +```bash +git add packages/rum-/ +git commit -m "✨ Add router integration package + +Auto-generated from framework documentation using the router integration pipeline. +See docs/integrations// for design artifacts and decision rationale." +``` + +For `extend-existing`: + +```bash +git add packages// +git commit -m "✨ Add router support to + +Auto-generated from framework documentation using the router integration pipeline. +See docs/integrations// for design artifacts and decision rationale." +``` + +### 4. Push and Create Draft PR + +```bash +git push -u origin "$BRANCH" +``` + +Then use `gh pr create` with this structure: + +```bash +gh pr create --draft --title "✨ Add router integration" --body "$(cat <<'PREOF' +## Summary + +Auto-generated router integration for using the router integration pipeline. + +- Router view tracking via `startView()` on route changes +- Unit tests for view name computation and navigation filtering + +## Design Artifacts + +Full design trail at `docs/integrations//`: +- `01-router-concepts.json` — structured routing concepts extracted from framework docs +- `02-design-decisions.json` — hook selection, wrapping strategy, algorithm family decisions +- `03-generation-manifest.md` — list of generated files with lineage + +## Key Decisions + + + +## Test plan + +- [ ] Review design artifacts in `docs/integrations//` +- [ ] Review generated code against reference implementations +- [ ] Run `yarn typecheck` to verify type correctness +- [ ] Run `yarn test:unit --spec packages/rum-/` to verify tests pass +- [ ] Manual review of `computeViewName()` edge cases + +🤖 Generated with the router integration pipeline +PREOF +)" +``` + +Replace `` with the actual framework name. Replace the key decisions placeholder with actual content from `02-design-decisions.json`. + +## Output + +Report the PR URL to the user. diff --git a/.gitignore b/.gitignore index af4e781f89..0bf48db32e 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ playwright-report/ # Claude Code local files *.local.md .claude/settings.local.json +.worktrees/