@hover-dev/core 0.14.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +73 -1
  2. package/dist/agents/claude.d.ts.map +1 -1
  3. package/dist/agents/claude.js +14 -0
  4. package/dist/agents/codex.d.ts.map +1 -1
  5. package/dist/agents/codex.js +1 -0
  6. package/dist/agents/invoke.d.ts.map +1 -1
  7. package/dist/agents/invoke.js +10 -1
  8. package/dist/agents/types.d.ts +11 -0
  9. package/dist/agents/types.d.ts.map +1 -1
  10. package/dist/playwright/resolveMcpConfig.d.ts +5 -0
  11. package/dist/playwright/resolveMcpConfig.d.ts.map +1 -1
  12. package/dist/playwright/resolveMcpConfig.js +2 -1
  13. package/dist/runSession.d.ts +42 -0
  14. package/dist/runSession.d.ts.map +1 -0
  15. package/dist/runSession.js +76 -0
  16. package/dist/service/cdpHint.d.ts.map +1 -1
  17. package/dist/service/cdpHint.js +30 -14
  18. package/dist/service/conventions.d.ts +8 -0
  19. package/dist/service/conventions.d.ts.map +1 -0
  20. package/dist/service/conventions.js +42 -0
  21. package/dist/service/saveHandlers.d.ts +10 -13
  22. package/dist/service/saveHandlers.d.ts.map +1 -1
  23. package/dist/service/saveHandlers.js +9 -25
  24. package/dist/service/types.d.ts +5 -0
  25. package/dist/service/types.d.ts.map +1 -1
  26. package/dist/service.d.ts +7 -4
  27. package/dist/service.d.ts.map +1 -1
  28. package/dist/service.js +141 -104
  29. package/dist/skills/writeSkill.d.ts +12 -35
  30. package/dist/skills/writeSkill.d.ts.map +1 -1
  31. package/dist/skills/writeSkill.js +10 -166
  32. package/dist/specs/detectSharedFlows.d.ts +35 -0
  33. package/dist/specs/detectSharedFlows.d.ts.map +1 -0
  34. package/dist/specs/detectSharedFlows.js +171 -0
  35. package/dist/specs/extractPageObjects.d.ts +18 -0
  36. package/dist/specs/extractPageObjects.d.ts.map +1 -0
  37. package/dist/specs/extractPageObjects.js +98 -0
  38. package/dist/specs/generatePageObject.d.ts +29 -0
  39. package/dist/specs/generatePageObject.d.ts.map +1 -0
  40. package/dist/specs/generatePageObject.js +149 -0
  41. package/dist/specs/listSpecs.d.ts +12 -0
  42. package/dist/specs/listSpecs.d.ts.map +1 -1
  43. package/dist/specs/listSpecs.js +27 -2
  44. package/dist/specs/optimizationSuggestion.d.ts +26 -0
  45. package/dist/specs/optimizationSuggestion.d.ts.map +1 -0
  46. package/dist/specs/optimizationSuggestion.js +28 -0
  47. package/dist/specs/optimizeSpec.d.ts +42 -0
  48. package/dist/specs/optimizeSpec.d.ts.map +1 -0
  49. package/dist/specs/optimizeSpec.js +166 -0
  50. package/dist/specs/optimizeSpecWithAgent.d.ts +11 -0
  51. package/dist/specs/optimizeSpecWithAgent.d.ts.map +1 -0
  52. package/dist/specs/optimizeSpecWithAgent.js +40 -0
  53. package/dist/specs/pageObjectManifest.d.ts +20 -0
  54. package/dist/specs/pageObjectManifest.d.ts.map +1 -0
  55. package/dist/specs/pageObjectManifest.js +40 -0
  56. package/dist/specs/seeds.d.ts +36 -0
  57. package/dist/specs/seeds.d.ts.map +1 -0
  58. package/dist/specs/seeds.js +74 -0
  59. package/dist/specs/sidecar.d.ts +25 -0
  60. package/dist/specs/sidecar.d.ts.map +1 -0
  61. package/dist/specs/sidecar.js +38 -0
  62. package/dist/specs/writeSpec.d.ts +50 -0
  63. package/dist/specs/writeSpec.d.ts.map +1 -1
  64. package/dist/specs/writeSpec.js +249 -75
  65. package/package.json +1 -2
@@ -0,0 +1,42 @@
1
+ import { type SpecSidecar } from './sidecar.js';
2
+ import { type SeedRule } from './seeds.js';
3
+ export declare class OptimizeError extends Error {
4
+ constructor(message: string);
5
+ }
6
+ /** Runs the codegen LLM on a prompt and returns its raw text output. */
7
+ export type RunCodegen = (prompt: string) => Promise<string>;
8
+ export interface OptimizeResult {
9
+ /** Absolute path of the written candidate (never the original spec). */
10
+ candidatePath: string;
11
+ /** The validated candidate source. */
12
+ code: string;
13
+ /** The original (deterministic) spec the candidate was generated from —
14
+ * returned so callers can show a diff without re-reading the file. */
15
+ original: string;
16
+ }
17
+ export declare function optimizeSpec(devRoot: string, slug: string, runCodegen: RunCodegen): Promise<OptimizeResult>;
18
+ /**
19
+ * Build the codegen prompt: the current spec + the observed session, plus the
20
+ * same rules the deterministic path enforces (semantic selectors, no XPath, no
21
+ * waitForTimeout, keep the test.step shape).
22
+ */
23
+ export declare function buildOptimizePrompt(draft: string, sidecar: SpecSidecar | null, seeds?: SeedRule[]): string;
24
+ /** Strip a ```ts fence if the model wrapped its output in one. */
25
+ export declare function extractCode(raw: string): string;
26
+ /**
27
+ * Code-level guardrails — the same constraints the deterministic path keeps,
28
+ * enforced on the LLM's output so an optimization can't drift off-policy. This
29
+ * is what lets us allow an LLM to author here without a markdown constitution.
30
+ */
31
+ export declare function validateSpecCode(code: string): {
32
+ ok: boolean;
33
+ errors: string[];
34
+ };
35
+ /** Promote an optimization candidate to the real spec (overwriting it) and
36
+ * remove the candidate. Returns the written spec path. The human's "Use
37
+ * optimized" / `mv` action. */
38
+ export declare function promoteOptimized(devRoot: string, slug: string): Promise<string>;
39
+ /** Discard an optimization candidate (delete the .draft, leave the spec). The
40
+ * human's "Keep original". */
41
+ export declare function discardOptimized(devRoot: string, slug: string): Promise<void>;
42
+ //# sourceMappingURL=optimizeSpec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"optimizeSpec.d.ts","sourceRoot":"","sources":["../../src/specs/optimizeSpec.ts"],"names":[],"mappings":"AAcA,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAA4B,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAErE,qBAAa,aAAc,SAAQ,KAAK;gBAC1B,OAAO,EAAE,MAAM;CAI5B;AAED,wEAAwE;AACxE,MAAM,MAAM,UAAU,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAE7D,MAAM,WAAW,cAAc;IAC7B,wEAAwE;IACxE,aAAa,EAAE,MAAM,CAAC;IACtB,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb;2EACuE;IACvE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAsB,YAAY,CAChC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,cAAc,CAAC,CAsCzB;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,WAAW,GAAG,IAAI,EAC3B,KAAK,GAAE,QAAQ,EAAO,GACrB,MAAM,CAmDR;AAED,kEAAkE;AAClE,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAI/C;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAahF;AAMD;;gCAEgC;AAChC,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAYrF;AAED;+BAC+B;AAC/B,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEnF"}
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Stage 7 (F7): the optional LLM optimization pass.
3
+ *
4
+ * Reads a deterministic draft spec + its sidecar, asks an LLM (the codegen
5
+ * mode — no browser, no MCP) to improve it (chiefly: add assertions for the
6
+ * feedback the session observed), validates the result, and writes it as a
7
+ * CANDIDATE at `.hover/optimized/<slug>.spec.ts.draft` — never overwriting the
8
+ * original (D10). A human promotes or discards it via diff.
9
+ *
10
+ * The LLM call is injected (`runCodegen`) so callers wire their own agent and
11
+ * tests run deterministically without spawning anything.
12
+ */
13
+ import { readFile, mkdir, writeFile, rm } from 'node:fs/promises';
14
+ import { join } from 'node:path';
15
+ import { sidecarDir } from './sidecar.js';
16
+ import { readSeeds, relevantSeeds } from './seeds.js';
17
+ export class OptimizeError extends Error {
18
+ constructor(message) {
19
+ super(message);
20
+ this.name = 'OptimizeError';
21
+ }
22
+ }
23
+ export async function optimizeSpec(devRoot, slug, runCodegen) {
24
+ const specPath = join(devRoot, '__vibe_tests__', `${slug}.spec.ts`);
25
+ let draft;
26
+ try {
27
+ draft = await readFile(specPath, 'utf-8');
28
+ }
29
+ catch {
30
+ throw new OptimizeError(`spec not found: ${slug} (looked at ${specPath})`);
31
+ }
32
+ let sidecar = null;
33
+ try {
34
+ sidecar = JSON.parse(await readFile(join(sidecarDir(devRoot), `${slug}.json`), 'utf-8'));
35
+ }
36
+ catch {
37
+ /* no sidecar — optimize from the draft alone */
38
+ }
39
+ const specTools = new Set((sidecar?.steps ?? [])
40
+ .filter(s => s.kind === 'step' && s.tool)
41
+ .map(s => s.tool));
42
+ const seeds = relevantSeeds(await readSeeds(devRoot), specTools);
43
+ const raw = await runCodegen(buildOptimizePrompt(draft, sidecar, seeds));
44
+ const code = extractCode(raw);
45
+ const check = validateSpecCode(code);
46
+ if (!check.ok) {
47
+ throw new OptimizeError(`optimization rejected — ${check.errors.join('; ')}`);
48
+ }
49
+ const dir = join(devRoot, '__vibe_tests__', '.hover', 'optimized');
50
+ await mkdir(dir, { recursive: true });
51
+ // `.spec.ts.draft`, never `*.spec.ts` — Playwright's glob must not collect a
52
+ // candidate before a human reviews it.
53
+ const candidatePath = join(dir, `${slug}.spec.ts.draft`);
54
+ await writeFile(candidatePath, code.endsWith('\n') ? code : `${code}\n`, 'utf-8');
55
+ return { candidatePath, code, original: draft };
56
+ }
57
+ /**
58
+ * Build the codegen prompt: the current spec + the observed session, plus the
59
+ * same rules the deterministic path enforces (semantic selectors, no XPath, no
60
+ * waitForTimeout, keep the test.step shape).
61
+ */
62
+ export function buildOptimizePrompt(draft, sidecar, seeds = []) {
63
+ const done = sidecar?.steps.find(s => s.kind === 'done');
64
+ const stepsJson = sidecar
65
+ ? JSON.stringify(sidecar.steps.filter(s => s.kind === 'step'), null, 2)
66
+ : '(no sidecar captured)';
67
+ return [
68
+ `You are improving an already-correct, generated Playwright spec. You are`,
69
+ `given the current deterministic spec and the structured browser session it`,
70
+ `was crystallized from.`,
71
+ ``,
72
+ `Improve it WITHOUT changing what it tests:`,
73
+ ` - Add assertions for the success/error feedback the session OBSERVED —`,
74
+ ` e.g. await expect(page.getByText('Invalid email')).toBeVisible(), a`,
75
+ ` success toast, a counter value. Use the captured steps + the outcome`,
76
+ ` summary below to know what to assert.`,
77
+ ` - Keep semantic selectors: getByRole / getByLabel / getByText. NEVER emit`,
78
+ ` XPath or CSS-id selectors. NEVER use waitForTimeout (Playwright`,
79
+ ` auto-waits).`,
80
+ ` - Keep the existing import line and the test.step(...) structure.`,
81
+ ` - Do not invent steps the session did not perform.`,
82
+ ` - If an observed outcome looks like a BUG (it contradicts what a correct`,
83
+ ` app should do — a stale error that never clears, the wrong message, a`,
84
+ ` value that should have changed but didn't), STILL assert the observed`,
85
+ ` reality (Hover records what actually happened), but put a comment`,
86
+ ` "// KNOWN BUG: <one line>" on the line directly above that assertion so a`,
87
+ ` human can find it and so the test breaks loudly once the app is fixed.`,
88
+ ` Never silently lock buggy behavior into a normal-looking assertion.`,
89
+ ``,
90
+ `Output ONLY the complete .ts file contents — no markdown fences, no prose,`,
91
+ `no explanation.`,
92
+ ``,
93
+ `=== CURRENT SPEC ===`,
94
+ draft,
95
+ ``,
96
+ `=== OBSERVED OUTCOME ===`,
97
+ done?.summary?.trim() || '(none)',
98
+ ``,
99
+ `=== CAPTURED STEPS ===`,
100
+ stepsJson,
101
+ ...(seeds.length > 0
102
+ ? [
103
+ ``,
104
+ `=== WORKED EXAMPLES (apply a pattern ONLY if the steps genuinely match it) ===`,
105
+ ...seeds.map(s => `# ${s.name}${s.note ? ` — ${s.note}` : ''}\n` +
106
+ `WHEN steps look like: ${JSON.stringify(s.example.steps)}\n` +
107
+ `EMIT something like:\n${s.example.code}`),
108
+ ]
109
+ : []),
110
+ ].join('\n');
111
+ }
112
+ /** Strip a ```ts fence if the model wrapped its output in one. */
113
+ export function extractCode(raw) {
114
+ const t = raw.trim();
115
+ const fence = t.match(/```(?:ts|typescript|tsx|javascript|js)?\s*\n([\s\S]*?)```/);
116
+ return (fence ? fence[1] : t).trim();
117
+ }
118
+ /**
119
+ * Code-level guardrails — the same constraints the deterministic path keeps,
120
+ * enforced on the LLM's output so an optimization can't drift off-policy. This
121
+ * is what lets us allow an LLM to author here without a markdown constitution.
122
+ */
123
+ export function validateSpecCode(code) {
124
+ const errors = [];
125
+ if (!code.trim())
126
+ errors.push('empty output');
127
+ if (/\bwaitForTimeout\b/.test(code))
128
+ errors.push('uses waitForTimeout');
129
+ if (/xpath\s*=|locator\(\s*['"`]\/\//i.test(code))
130
+ errors.push('uses an XPath selector');
131
+ if (!/\btest\s*\(/.test(code))
132
+ errors.push('no test() block');
133
+ if (!/from\s+['"](@playwright\/test|\.\/fixtures)['"]/.test(code)) {
134
+ errors.push('missing @playwright/test (or ./fixtures) import');
135
+ }
136
+ const open = (code.match(/\{/g) ?? []).length;
137
+ const close = (code.match(/\}/g) ?? []).length;
138
+ if (open !== close)
139
+ errors.push('unbalanced braces');
140
+ return { ok: errors.length === 0, errors };
141
+ }
142
+ function candidatePathFor(devRoot, slug) {
143
+ return join(devRoot, '__vibe_tests__', '.hover', 'optimized', `${slug}.spec.ts.draft`);
144
+ }
145
+ /** Promote an optimization candidate to the real spec (overwriting it) and
146
+ * remove the candidate. Returns the written spec path. The human's "Use
147
+ * optimized" / `mv` action. */
148
+ export async function promoteOptimized(devRoot, slug) {
149
+ const candidate = candidatePathFor(devRoot, slug);
150
+ const specPath = join(devRoot, '__vibe_tests__', `${slug}.spec.ts`);
151
+ let code;
152
+ try {
153
+ code = await readFile(candidate, 'utf-8');
154
+ }
155
+ catch {
156
+ throw new OptimizeError(`no optimization candidate to promote for "${slug}"`);
157
+ }
158
+ await writeFile(specPath, code, 'utf-8');
159
+ await rm(candidate, { force: true });
160
+ return specPath;
161
+ }
162
+ /** Discard an optimization candidate (delete the .draft, leave the spec). The
163
+ * human's "Keep original". */
164
+ export async function discardOptimized(devRoot, slug) {
165
+ await rm(candidatePathFor(devRoot, slug), { force: true });
166
+ }
@@ -0,0 +1,11 @@
1
+ import { type OptimizeResult } from './optimizeSpec.js';
2
+ export interface OptimizeAgentOptions {
3
+ agentId: string;
4
+ model?: string;
5
+ maxBudgetUsd?: number;
6
+ /** Optional model API key, injected into the spawned CLI's env. */
7
+ apiKey?: string;
8
+ signal?: AbortSignal;
9
+ }
10
+ export declare function optimizeSpecWithAgent(devRoot: string, slug: string, opts: OptimizeAgentOptions): Promise<OptimizeResult>;
11
+ //# sourceMappingURL=optimizeSpecWithAgent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"optimizeSpecWithAgent.d.ts","sourceRoot":"","sources":["../../src/specs/optimizeSpecWithAgent.ts"],"names":[],"mappings":"AAUA,OAAO,EAAgB,KAAK,cAAc,EAAmB,MAAM,mBAAmB,CAAC;AAEvF,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,oBAAoB,GACzB,OAAO,CAAC,cAAc,CAAC,CA4BzB"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Wires optimizeSpec's injected codegen call to a real agent via invokeAgent,
3
+ * in "codegen mode": no MCP, no browser tools, the agent's own built-in tools
4
+ * disallowed — it just reads the prompt and emits the improved spec as text.
5
+ *
6
+ * Kept separate from optimizeSpec.ts so the core (prompt / extract / validate /
7
+ * write) stays a pure, spawn-free module that tests import directly.
8
+ */
9
+ import { invokeAgent } from '../agents/invoke.js';
10
+ import { getAgent } from '../agents/registry.js';
11
+ import { optimizeSpec } from './optimizeSpec.js';
12
+ export async function optimizeSpecWithAgent(devRoot, slug, opts) {
13
+ const descriptor = getAgent(opts.agentId);
14
+ // Codegen mode: deny the agent's built-in tools so it answers with text only;
15
+ // pass no mcpConfig / allowedTools so it never reaches a browser.
16
+ const disallowedTools = descriptor?.defaultDisallowedTools
17
+ ? [...descriptor.defaultDisallowedTools]
18
+ : undefined;
19
+ const runCodegen = async (prompt) => {
20
+ let streamed = '';
21
+ let summary = '';
22
+ for await (const ev of invokeAgent({
23
+ agentId: opts.agentId,
24
+ prompt,
25
+ model: opts.model,
26
+ maxBudgetUsd: opts.maxBudgetUsd,
27
+ apiKey: opts.apiKey,
28
+ signal: opts.signal,
29
+ disallowedTools,
30
+ })) {
31
+ if (ev.kind === 'text' && ev.text)
32
+ streamed += `${ev.text}\n`;
33
+ else if (ev.kind === 'session_end' && ev.summary)
34
+ summary = ev.summary;
35
+ }
36
+ // Prefer the final result summary; fall back to streamed text blocks.
37
+ return summary || streamed;
38
+ };
39
+ return optimizeSpec(devRoot, slug, runCodegen);
40
+ }
@@ -0,0 +1,20 @@
1
+ export declare const MANIFEST_VERSION = 1;
2
+ export interface PageObjectEntry {
3
+ className: string;
4
+ methodName: string;
5
+ /** Fixture key in fixtures.ts, e.g. `loginPage`. */
6
+ fixtureName: string;
7
+ fileName: string;
8
+ /** The signature prefix this Page Object's method replays (one per step). */
9
+ signatures: string[];
10
+ /** Slugs of the specs the Page Object was lifted from. */
11
+ specs: string[];
12
+ }
13
+ export interface PageObjectManifest {
14
+ version: number;
15
+ pages: PageObjectEntry[];
16
+ }
17
+ export declare function writePageObjectManifest(devRoot: string, pages: PageObjectEntry[]): Promise<string>;
18
+ /** Read the manifest, or null when none exists (no extraction has run). */
19
+ export declare function readPageObjectManifest(devRoot: string): Promise<PageObjectManifest | null>;
20
+ //# sourceMappingURL=pageObjectManifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pageObjectManifest.d.ts","sourceRoot":"","sources":["../../src/specs/pageObjectManifest.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAElC,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,0DAA0D;IAC1D,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,eAAe,EAAE,CAAC;CAC1B;AAMD,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,eAAe,EAAE,GACvB,OAAO,CAAC,MAAM,CAAC,CAOjB;AAED,2EAA2E;AAC3E,wBAAsB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAQhG"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Page Object manifest — the link between extraction (Stage 3b) and
3
+ * consumption (Stage 3c).
4
+ *
5
+ * extractPageObjects writes `.hover/page-objects.json` describing each emitted
6
+ * Page Object: its class/method/fixture names and the signature prefix it
7
+ * replays. writeSpec reads it to decide whether a freshly-saved spec's prefix
8
+ * matches a Page Object — if so it consumes `await loginPage.login(…)` and
9
+ * imports from `./fixtures` instead of re-emitting the steps inline.
10
+ *
11
+ * Kept separate from extractPageObjects so writeSpec can read the manifest
12
+ * without importing the detection/generation chain.
13
+ */
14
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
15
+ import { join } from 'node:path';
16
+ import { sidecarDir } from './sidecar.js';
17
+ export const MANIFEST_VERSION = 1;
18
+ function manifestPath(devRoot) {
19
+ return join(sidecarDir(devRoot), 'page-objects.json');
20
+ }
21
+ export async function writePageObjectManifest(devRoot, pages) {
22
+ const dir = sidecarDir(devRoot);
23
+ await mkdir(dir, { recursive: true });
24
+ const path = manifestPath(devRoot);
25
+ const manifest = { version: MANIFEST_VERSION, pages };
26
+ await writeFile(path, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
27
+ return path;
28
+ }
29
+ /** Read the manifest, or null when none exists (no extraction has run). */
30
+ export async function readPageObjectManifest(devRoot) {
31
+ try {
32
+ const m = JSON.parse(await readFile(manifestPath(devRoot), 'utf-8'));
33
+ if (Array.isArray(m.pages))
34
+ return m;
35
+ }
36
+ catch {
37
+ /* no manifest / malformed — treat as none */
38
+ }
39
+ return null;
40
+ }
@@ -0,0 +1,36 @@
1
+ export interface SeedRule {
2
+ /** Identifier, e.g. `download`. */
3
+ name: string;
4
+ /** Rough match signature — tool names (optionally `tool:detail`), used only
5
+ * to pick relevant seeds for a spec, NOT for exact matching. */
6
+ signature: string[];
7
+ /** One-line human note: what the pattern is / when it applies. */
8
+ note?: string;
9
+ /** A concrete worked example the LLM generalizes from. */
10
+ example: {
11
+ steps: unknown[];
12
+ code: string;
13
+ };
14
+ }
15
+ /**
16
+ * Built-in seeds ship with core and feed EVERY project's optimization pass, so
17
+ * the bar is high: a pattern qualifies as built-in ONLY if it's a *highly
18
+ * certain* optimization — a fixed, app-agnostic translation whose output is
19
+ * deterministic and can't mislead (e.g. download → waitForEvent pairing).
20
+ *
21
+ * Deliberately NOT built-in:
22
+ * - Semantic / judgement-based optimizations (e.g. WHICH feedback text to
23
+ * assert) — those are already standing instructions in buildOptimizePrompt,
24
+ * and a bad generalization would pollute every user's spec.
25
+ * - Popup/new-tab — hardcoded in the translator (writeSpec), not a seed.
26
+ * Project-specific or speculative patterns live in <root>/.hover/rules/, where
27
+ * the bar is the user's own call.
28
+ */
29
+ export declare const BUILTIN_SEEDS: SeedRule[];
30
+ /** Built-in seeds + any in <projectRoot>/.hover/rules/*.json. Malformed files
31
+ * are skipped rather than failing the whole read. */
32
+ export declare function readSeeds(projectRoot: string): Promise<SeedRule[]>;
33
+ /** Pick seeds whose signature's base tool appears in the spec — a cheap
34
+ * relevance filter so the prompt only carries plausibly-applicable examples. */
35
+ export declare function relevantSeeds(seeds: SeedRule[], specTools: Set<string>, cap?: number): SeedRule[];
36
+ //# sourceMappingURL=seeds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seeds.d.ts","sourceRoot":"","sources":["../../src/specs/seeds.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,QAAQ;IACvB,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAC;IACb;qEACiE;IACjE,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,kEAAkE;IAClE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0DAA0D;IAC1D,OAAO,EAAE;QAAE,KAAK,EAAE,OAAO,EAAE,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7C;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,aAAa,EAAE,QAAQ,EAenC,CAAC;AAEF;sDACsD;AACtD,wBAAsB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAiBxE;AAED;iFACiF;AACjF,wBAAgB,aAAa,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,GAAG,SAAI,GAAG,QAAQ,EAAE,CAG5F"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Community translation seeds (Stage 6, approach A): human-written worked
3
+ * examples that teach the optimization pass (F7) new patterns by few-shot,
4
+ * NOT by deterministic match+template. A seed is a rough `signature` (tool
5
+ * names, used only to pick relevant seeds) + a concrete `example`
6
+ * (input steps → output code) the LLM generalizes from.
7
+ *
8
+ * Sources: a small built-in set + the project's <projectRoot>/.hover/rules/.
9
+ * Adding a pattern = dropping an example JSON in .hover/rules/ — no core change.
10
+ *
11
+ * Built-in seeds deliberately cover patterns the deterministic translator does
12
+ * NOT hardcode (popup is already hardcoded in writeSpec, so it's not here).
13
+ */
14
+ import { readFile, readdir } from 'node:fs/promises';
15
+ import { join } from 'node:path';
16
+ /**
17
+ * Built-in seeds ship with core and feed EVERY project's optimization pass, so
18
+ * the bar is high: a pattern qualifies as built-in ONLY if it's a *highly
19
+ * certain* optimization — a fixed, app-agnostic translation whose output is
20
+ * deterministic and can't mislead (e.g. download → waitForEvent pairing).
21
+ *
22
+ * Deliberately NOT built-in:
23
+ * - Semantic / judgement-based optimizations (e.g. WHICH feedback text to
24
+ * assert) — those are already standing instructions in buildOptimizePrompt,
25
+ * and a bad generalization would pollute every user's spec.
26
+ * - Popup/new-tab — hardcoded in the translator (writeSpec), not a seed.
27
+ * Project-specific or speculative patterns live in <root>/.hover/rules/, where
28
+ * the bar is the user's own call.
29
+ */
30
+ export const BUILTIN_SEEDS = [
31
+ {
32
+ name: 'download',
33
+ signature: ['browser_click'],
34
+ note: 'a click that triggers a file download → pair with waitForEvent("download")',
35
+ example: {
36
+ steps: [{ tool: 'browser_click', element: 'Export CSV button' }],
37
+ code: "const [download] = await Promise.all([\n" +
38
+ " page.waitForEvent('download'),\n" +
39
+ " page.getByRole('button', { name: 'Export CSV' }).click(),\n" +
40
+ "]);\n" +
41
+ "expect(await download.suggestedFilename()).toContain('.csv');",
42
+ },
43
+ },
44
+ ];
45
+ /** Built-in seeds + any in <projectRoot>/.hover/rules/*.json. Malformed files
46
+ * are skipped rather than failing the whole read. */
47
+ export async function readSeeds(projectRoot) {
48
+ const out = [...BUILTIN_SEEDS];
49
+ try {
50
+ const dir = join(projectRoot, '.hover', 'rules');
51
+ for (const f of await readdir(dir)) {
52
+ if (!f.endsWith('.json'))
53
+ continue;
54
+ try {
55
+ const s = JSON.parse(await readFile(join(dir, f), 'utf-8'));
56
+ if (s && s.name && Array.isArray(s.signature) && s.example?.code)
57
+ out.push(s);
58
+ }
59
+ catch {
60
+ /* skip malformed seed file */
61
+ }
62
+ }
63
+ }
64
+ catch {
65
+ /* no .hover/rules/ directory */
66
+ }
67
+ return out;
68
+ }
69
+ /** Pick seeds whose signature's base tool appears in the spec — a cheap
70
+ * relevance filter so the prompt only carries plausibly-applicable examples. */
71
+ export function relevantSeeds(seeds, specTools, cap = 6) {
72
+ const hits = seeds.filter(s => s.signature.some(sig => specTools.has(sig.split(':')[0])));
73
+ return hits.slice(0, cap);
74
+ }
@@ -0,0 +1,25 @@
1
+ import type { SkillStep } from '../skills/writeSkill.js';
2
+ import type { SpecAssertion } from './writeSpec.js';
3
+ /** Current sidecar schema version. Bump when the shape changes so readers
4
+ * (Stage 2 detection, Stage 7 optimization) can migrate or skip cleanly. */
5
+ export declare const SIDECAR_VERSION = 1;
6
+ export interface SpecSidecar {
7
+ version: number;
8
+ slug: string;
9
+ name: string;
10
+ /** ISO timestamp the sidecar was written. */
11
+ createdAt: string;
12
+ /** The full captured session, structured and verbatim — never re-derived
13
+ * from the generated `.spec.ts`. */
14
+ steps: SkillStep[];
15
+ /** Alt-click assertions captured alongside the session. */
16
+ assertions: SpecAssertion[];
17
+ }
18
+ /** Sidecar directory under the spec output dir. Dot-prefixed on purpose:
19
+ * Playwright's default `*.spec.ts` glob never reaches into `.hover/`. */
20
+ export declare function sidecarDir(devRoot: string): string;
21
+ /** Write the structured-session sidecar at `.hover/<slug>.json`. Caller passes
22
+ * the data minus the stamped fields (`version`, `createdAt`), which this
23
+ * function fills. Returns the absolute path written. */
24
+ export declare function writeSidecar(devRoot: string, data: Omit<SpecSidecar, 'version' | 'createdAt'>): Promise<string>;
25
+ //# sourceMappingURL=sidecar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sidecar.d.ts","sourceRoot":"","sources":["../../src/specs/sidecar.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpD;6EAC6E;AAC7E,eAAO,MAAM,eAAe,IAAI,CAAC;AAEjC,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB;yCACqC;IACrC,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,2DAA2D;IAC3D,UAAU,EAAE,aAAa,EAAE,CAAC;CAC7B;AAED;0EAC0E;AAC1E,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAElD;AAED;;yDAEyD;AACzD,wBAAsB,YAAY,CAChC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,SAAS,GAAG,WAAW,CAAC,GAC/C,OAAO,CAAC,MAAM,CAAC,CAWjB"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Structured-session sidecar for a generated spec.
3
+ *
4
+ * Every saved spec gets a companion `__vibe_tests__/.hover/<slug>.json` holding
5
+ * the original structured `SpecStep[]` (plus assertions + metadata). The
6
+ * `.spec.ts` is the human / CI artifact; this sidecar is the machine record
7
+ * that downstream work reads instead of parsing generated code:
8
+ * - F4 cross-session extraction signature-matches `steps` across sidecars.
9
+ * - F7 optimization pass feeds the draft + this sidecar to the LLM.
10
+ *
11
+ * It lands in a dot-prefixed `.hover/` dir so Playwright's default `*.spec.ts`
12
+ * glob never collects it, and it is pure data — no Hover runtime import.
13
+ */
14
+ import { mkdir, writeFile } from 'node:fs/promises';
15
+ import { join } from 'node:path';
16
+ /** Current sidecar schema version. Bump when the shape changes so readers
17
+ * (Stage 2 detection, Stage 7 optimization) can migrate or skip cleanly. */
18
+ export const SIDECAR_VERSION = 1;
19
+ /** Sidecar directory under the spec output dir. Dot-prefixed on purpose:
20
+ * Playwright's default `*.spec.ts` glob never reaches into `.hover/`. */
21
+ export function sidecarDir(devRoot) {
22
+ return join(devRoot, '__vibe_tests__', '.hover');
23
+ }
24
+ /** Write the structured-session sidecar at `.hover/<slug>.json`. Caller passes
25
+ * the data minus the stamped fields (`version`, `createdAt`), which this
26
+ * function fills. Returns the absolute path written. */
27
+ export async function writeSidecar(devRoot, data) {
28
+ const dir = sidecarDir(devRoot);
29
+ await mkdir(dir, { recursive: true });
30
+ const path = join(dir, `${data.slug}.json`);
31
+ const sidecar = {
32
+ version: SIDECAR_VERSION,
33
+ createdAt: new Date().toISOString(),
34
+ ...data,
35
+ };
36
+ await writeFile(path, JSON.stringify(sidecar, null, 2) + '\n', 'utf-8');
37
+ return path;
38
+ }
@@ -1,5 +1,18 @@
1
1
  import type { SkillStep } from '../skills/writeSkill.js';
2
2
  export type SpecStep = SkillStep;
3
+ /**
4
+ * Marker the deterministic translator leaves where a captured action is a real
5
+ * interaction but has no single-step Playwright translation (e.g. file upload,
6
+ * drag, a dialog handler) — a shape that needs a multi-step pattern. It is a
7
+ * structured signal, not a `// TODO`: the optimization pass (F7) and the
8
+ * "seeds could complete this — review?" suggestion grep for it, and
9
+ * `countOptimizableMarkers` reads it back off a saved spec.
10
+ */
11
+ export declare const OPTIMIZABLE_MARKER = "// hover:optimizable";
12
+ /** How many `// hover:optimizable` markers a generated spec carries. Used to
13
+ * surface "this spec has an interaction the deterministic pass couldn't fully
14
+ * translate — the optimization pass can complete it". */
15
+ export declare function countOptimizableMarkers(source: string): number;
3
16
  export interface SpecAssertion {
4
17
  /** Generated Playwright code (single line, no leading "await "). */
5
18
  code: string;
@@ -24,4 +37,41 @@ export interface WriteSpecResult {
24
37
  slug: string;
25
38
  }
26
39
  export declare function writeSpec(opts: WriteSpecOptions): Promise<WriteSpecResult>;
40
+ /**
41
+ * Emit an interaction (click / dblclick / hover / fill / selectOption) as a
42
+ * visibility-guarded prelude: hoist the locator to `el`, assert it's visible,
43
+ * then act.
44
+ *
45
+ * const el = page.getByRole('button', { name: 'Submit' });
46
+ * await expect(el).toBeVisible();
47
+ * await el.click();
48
+ *
49
+ * Why: `getByRole` is "visible OR attached" by default. A button that drifted
50
+ * behind a closed `<details>` / kebab menu / drawer is still in the role tree,
51
+ * so the locator stays green AND `.click()` may still fire — but the actual
52
+ * user flow has degraded. Asserting visibility first makes that drift fail
53
+ * loudly with "Locator expected to be visible" instead of silently passing.
54
+ *
55
+ * Scoping: renderSpec wraps each step in its own `test.step(… async () => {})`,
56
+ * whose closure already scopes `el`, so a single interaction needs no extra
57
+ * braces. browser_fill_form emits several fields into one step, so it wraps
58
+ * each field in `blockScope(...)` to keep the per-field `el` from colliding.
59
+ */
60
+ export declare function emitInteraction(selectorExpr: string, action: string): string[];
61
+ /** Wrap lines in a `{ … }` block scope (2-space inner indent). Used by
62
+ * browser_fill_form so each field's `const el` lives in its own scope inside
63
+ * the shared test.step closure. */
64
+ export declare function blockScope(lines: string[]): string[];
65
+ /**
66
+ * Parse element descriptions like "Submit button" / "+1 button" / "Email
67
+ * textbox" / "Plan radio" into `getByRole(role, { name })` selectors. The
68
+ * trailing role keyword is the convention Playwright MCP uses.
69
+ */
70
+ export declare function selectorFromDescription(desc: string, pageVar?: string): string;
71
+ /**
72
+ * Form fields from browser_fill_form have a `name` that's typically the
73
+ * accessible name / label / aria-label. getByLabel is the right primitive.
74
+ * Fall back to getByRole('textbox') if we have a hint.
75
+ */
76
+ export declare function selectorForFormField(name: string, type?: string, pageVar?: string): string;
27
77
  //# sourceMappingURL=writeSpec.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"writeSpec.d.ts","sourceRoot":"","sources":["../../src/specs/writeSpec.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAGzD,MAAM,MAAM,QAAQ,GAAG,SAAS,CAAC;AAEjC,MAAM,WAAW,aAAa;IAC5B,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,eAAgB,SAAQ,KAAK;aACZ,IAAI,EAAE,MAAM;aAAkB,IAAI,EAAE,MAAM;gBAA1C,IAAI,EAAE,MAAM,EAAkB,IAAI,EAAE,MAAM;CAIvE;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,UAAU,CAAC,EAAE,aAAa,EAAE,CAAC;IAC7B,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAAG,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;CAAE;AAEhE,wBAAsB,SAAS,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CAkBhF"}
1
+ {"version":3,"file":"writeSpec.d.ts","sourceRoot":"","sources":["../../src/specs/writeSpec.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAUzD,MAAM,MAAM,QAAQ,GAAG,SAAS,CAAC;AAEjC;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,yBAAyB,CAAC;AAEzD;;0DAE0D;AAC1D,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED,MAAM,WAAW,aAAa;IAC5B,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,eAAgB,SAAQ,KAAK;aACZ,IAAI,EAAE,MAAM;aAAkB,IAAI,EAAE,MAAM;gBAA1C,IAAI,EAAE,MAAM,EAAkB,IAAI,EAAE,MAAM;CAIvE;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,UAAU,CAAC,EAAE,aAAa,EAAE,CAAC;IAC7B,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAAG,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;CAAE;AAEhE,wBAAsB,SAAS,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CAiChF;AAgVD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAM9E;AAED;;oCAEoC;AACpC,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAEpD;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,SAAS,GAAG,MAAM,CAqB9E;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,OAAO,SAAS,GAAG,MAAM,CAQ1F"}