@hover-dev/core 0.16.0 → 0.17.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.
- package/README.md +26 -55
- package/dist/agentDirectives.d.ts +55 -0
- package/dist/agentDirectives.d.ts.map +1 -0
- package/dist/agentDirectives.js +276 -0
- package/dist/agents/claude.d.ts.map +1 -1
- package/dist/agents/claude.js +28 -3
- package/dist/agents/codex.d.ts.map +1 -1
- package/dist/agents/codex.js +29 -14
- package/dist/agents/invoke.d.ts.map +1 -1
- package/dist/agents/invoke.js +3 -6
- package/dist/agents/registry.d.ts.map +1 -1
- package/dist/agents/registry.js +0 -4
- package/dist/agents/types.d.ts +19 -11
- package/dist/agents/types.d.ts.map +1 -1
- package/dist/engine.d.ts +53 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +78 -0
- package/dist/mcp/actuateServer.d.ts +3 -0
- package/dist/mcp/actuateServer.d.ts.map +1 -0
- package/dist/mcp/actuateServer.js +594 -0
- package/dist/mcp/sourceFence.d.ts.map +1 -1
- package/dist/mcp/sourceFence.js +4 -0
- package/dist/mcp/sourceServer.js +75 -0
- package/dist/memory/businessMemory.d.ts +29 -0
- package/dist/memory/businessMemory.d.ts.map +1 -0
- package/dist/memory/businessMemory.js +125 -0
- package/dist/modes.d.ts +39 -0
- package/dist/modes.d.ts.map +1 -0
- package/dist/modes.js +34 -0
- package/dist/playwright/cdpStatus.d.ts +0 -15
- package/dist/playwright/cdpStatus.d.ts.map +1 -1
- package/dist/playwright/cdpStatus.js +0 -67
- package/dist/playwright/launchChrome.d.ts +18 -0
- package/dist/playwright/launchChrome.d.ts.map +1 -1
- package/dist/playwright/launchChrome.js +46 -3
- package/dist/playwright/resolveMcpConfig.d.ts +7 -1
- package/dist/playwright/resolveMcpConfig.d.ts.map +1 -1
- package/dist/playwright/resolveMcpConfig.js +22 -4
- package/dist/plugin-api.d.ts +28 -26
- package/dist/plugin-api.d.ts.map +1 -1
- package/dist/plugin-api.js +2 -2
- package/dist/qa/candidates.d.ts +32 -0
- package/dist/qa/candidates.d.ts.map +1 -0
- package/dist/qa/candidates.js +20 -0
- package/dist/qa/classify.d.ts +38 -0
- package/dist/qa/classify.d.ts.map +1 -0
- package/dist/qa/classify.js +138 -0
- package/dist/qa/intensity.d.ts +33 -0
- package/dist/qa/intensity.d.ts.map +1 -0
- package/dist/qa/intensity.js +25 -0
- package/dist/qa/qaReport.d.ts +19 -0
- package/dist/qa/qaReport.d.ts.map +1 -0
- package/dist/qa/qaReport.js +50 -0
- package/dist/runSession.d.ts +14 -3
- package/dist/runSession.d.ts.map +1 -1
- package/dist/runSession.js +26 -11
- package/dist/service/cdpHandlers.d.ts +1 -21
- package/dist/service/cdpHandlers.d.ts.map +1 -1
- package/dist/service/cdpHandlers.js +4 -39
- package/dist/service/cdpHint.d.ts +21 -28
- package/dist/service/cdpHint.d.ts.map +1 -1
- package/dist/service/cdpHint.js +106 -164
- package/dist/service/relayHandlers.d.ts +28 -0
- package/dist/service/relayHandlers.d.ts.map +1 -0
- package/dist/service/relayHandlers.js +105 -0
- package/dist/service/saveHandlers.d.ts +1 -3
- package/dist/service/saveHandlers.d.ts.map +1 -1
- package/dist/service/saveHandlers.js +17 -15
- package/dist/service/types.d.ts +108 -8
- package/dist/service/types.d.ts.map +1 -1
- package/dist/service.d.ts +7 -3
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +907 -200
- package/dist/sessions/sessions.d.ts +125 -0
- package/dist/sessions/sessions.d.ts.map +1 -0
- package/dist/sessions/sessions.js +175 -0
- package/dist/specs/authFixture.d.ts +30 -0
- package/dist/specs/authFixture.d.ts.map +1 -0
- package/dist/specs/authFixture.js +145 -0
- package/dist/specs/businessMap.d.ts +29 -0
- package/dist/specs/businessMap.d.ts.map +1 -0
- package/dist/specs/businessMap.js +95 -0
- package/dist/specs/detectSharedFlows.d.ts +1 -1
- package/dist/specs/detectSharedFlows.d.ts.map +1 -1
- package/dist/specs/detectSharedFlows.js +20 -21
- package/dist/specs/generatePageObject.d.ts +1 -1
- package/dist/specs/generatePageObject.d.ts.map +1 -1
- package/dist/specs/healPrompt.d.ts +19 -0
- package/dist/specs/healPrompt.d.ts.map +1 -0
- package/dist/specs/healPrompt.js +48 -0
- package/dist/specs/humanSteps.d.ts +4 -8
- package/dist/specs/humanSteps.d.ts.map +1 -1
- package/dist/specs/humanSteps.js +6 -1
- package/dist/specs/optimizeSpec.d.ts +15 -8
- package/dist/specs/optimizeSpec.d.ts.map +1 -1
- package/dist/specs/optimizeSpec.js +71 -41
- package/dist/specs/optimizeSpecWithAgent.d.ts +0 -2
- package/dist/specs/optimizeSpecWithAgent.d.ts.map +1 -1
- package/dist/specs/optimizeSpecWithAgent.js +0 -1
- package/dist/specs/pageObjectManifest.d.ts +3 -1
- package/dist/specs/pageObjectManifest.d.ts.map +1 -1
- package/dist/specs/pageObjectManifest.js +13 -9
- package/dist/specs/replayGrounded.d.ts +45 -0
- package/dist/specs/replayGrounded.d.ts.map +1 -0
- package/dist/specs/replayGrounded.js +155 -0
- package/dist/specs/runFailures.d.ts +34 -0
- package/dist/specs/runFailures.d.ts.map +1 -0
- package/dist/specs/runFailures.js +93 -0
- package/dist/specs/seeds.d.ts +16 -15
- package/dist/specs/seeds.d.ts.map +1 -1
- package/dist/specs/seeds.js +86 -54
- package/dist/specs/sidecar.d.ts +34 -6
- package/dist/specs/sidecar.d.ts.map +1 -1
- package/dist/specs/sidecar.js +79 -9
- package/dist/specs/specStep.d.ts +21 -0
- package/dist/specs/specStep.d.ts.map +1 -0
- package/dist/specs/specStep.js +1 -0
- package/dist/specs/text.d.ts +8 -6
- package/dist/specs/text.d.ts.map +1 -1
- package/dist/specs/text.js +10 -7
- package/dist/specs/writeSpec.d.ts +62 -1
- package/dist/specs/writeSpec.d.ts.map +1 -1
- package/dist/specs/writeSpec.js +596 -21
- package/package.json +6 -9
- package/dist/agents/aider.d.ts +0 -16
- package/dist/agents/aider.d.ts.map +0 -1
- package/dist/agents/aider.js +0 -161
- package/dist/agents/cursor.d.ts +0 -18
- package/dist/agents/cursor.d.ts.map +0 -1
- package/dist/agents/cursor.js +0 -220
- package/dist/playwright/raiseWindow.d.ts +0 -10
- package/dist/playwright/raiseWindow.d.ts.map +0 -1
- package/dist/playwright/raiseWindow.js +0 -158
- package/dist/scripts/bench-multi-tab.d.ts +0 -2
- package/dist/scripts/bench-multi-tab.d.ts.map +0 -1
- package/dist/scripts/bench-multi-tab.js +0 -192
- package/dist/scripts/bench-ttfb.d.ts +0 -2
- package/dist/scripts/bench-ttfb.d.ts.map +0 -1
- package/dist/scripts/bench-ttfb.js +0 -127
- package/dist/scripts/start-chrome.d.ts +0 -3
- package/dist/scripts/start-chrome.d.ts.map +0 -1
- package/dist/scripts/start-chrome.js +0 -23
- package/dist/skills/writeSkill.d.ts +0 -27
- package/dist/skills/writeSkill.d.ts.map +0 -1
- package/dist/skills/writeSkill.js +0 -13
- package/dist/specs/listSpecs.d.ts +0 -52
- package/dist/specs/listSpecs.d.ts.map +0 -1
- package/dist/specs/listSpecs.js +0 -139
- package/dist/specs/optimizationSuggestion.d.ts +0 -26
- package/dist/specs/optimizationSuggestion.d.ts.map +0 -1
- package/dist/specs/optimizationSuggestion.js +0 -28
- package/dist/specs/writeCaseCsv.d.ts +0 -28
- package/dist/specs/writeCaseCsv.d.ts.map +0 -1
- package/dist/specs/writeCaseCsv.js +0 -134
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { chromium } from 'playwright-core';
|
|
2
|
+
const ACTUATION_TOOLS = new Set(['click_control', 'fill_control', 'select_control', 'check_control', 'assert_visible']);
|
|
3
|
+
const ACTION_TIMEOUT = 8000;
|
|
4
|
+
function bare(tool) {
|
|
5
|
+
if (!tool)
|
|
6
|
+
return '';
|
|
7
|
+
const p = tool.split('__');
|
|
8
|
+
return p[0] === 'mcp' && p.length >= 3 ? p.slice(2).join('__') : tool;
|
|
9
|
+
}
|
|
10
|
+
function errLine(e) {
|
|
11
|
+
return e instanceof Error ? e.message.split('\n')[0] : String(e);
|
|
12
|
+
}
|
|
13
|
+
function originOf(u) {
|
|
14
|
+
try {
|
|
15
|
+
return new URL(u).origin;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function describe(g) {
|
|
22
|
+
if (g.role && g.name)
|
|
23
|
+
return `${g.role} "${g.name}"`;
|
|
24
|
+
if (g.testId)
|
|
25
|
+
return `testId "${g.testId}"`;
|
|
26
|
+
if (g.text)
|
|
27
|
+
return `text "${g.text}"`;
|
|
28
|
+
return '(no target)';
|
|
29
|
+
}
|
|
30
|
+
// Mirrors mcp/actuateServer.ts `locate` — role+name → testId → text, optionally
|
|
31
|
+
// scoped to a `within` container. See file header. Exported so non-extension
|
|
32
|
+
// consumers (the replayer here, the standalone `hover-mcp`) share ONE grounded
|
|
33
|
+
// resolver without touching the load-bearing actuateServer.
|
|
34
|
+
export function groundedLocate(page, g) {
|
|
35
|
+
const base = g.within?.role && g.within?.name
|
|
36
|
+
? page.getByRole(g.within.role, { name: g.within.name, exact: true })
|
|
37
|
+
: page;
|
|
38
|
+
if (g.role && g.name)
|
|
39
|
+
return base.getByRole(g.role, { name: g.name, exact: true });
|
|
40
|
+
if (g.testId)
|
|
41
|
+
return base.getByTestId(g.testId);
|
|
42
|
+
if (g.text)
|
|
43
|
+
return base.getByText(g.text).first();
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
/** Replay ONE grounded step on a page. Returns 'skipped' for non-actuation
|
|
47
|
+
* steps; throws on a failed locate / action (the caller records it). */
|
|
48
|
+
export async function applyGroundedStep(page, step) {
|
|
49
|
+
const tool = bare(step.tool);
|
|
50
|
+
if (!ACTUATION_TOOLS.has(tool))
|
|
51
|
+
return 'skipped';
|
|
52
|
+
const input = (step.input ?? {});
|
|
53
|
+
const loc = groundedLocate(page, input);
|
|
54
|
+
if (!loc)
|
|
55
|
+
throw new Error(`could not locate ${describe(input)}`);
|
|
56
|
+
switch (tool) {
|
|
57
|
+
case 'click_control':
|
|
58
|
+
await loc.click({ timeout: ACTION_TIMEOUT });
|
|
59
|
+
return 'ok';
|
|
60
|
+
case 'fill_control':
|
|
61
|
+
await loc.fill(String(input.value ?? ''), { timeout: ACTION_TIMEOUT });
|
|
62
|
+
return 'ok';
|
|
63
|
+
case 'select_control':
|
|
64
|
+
await loc.selectOption(String(input.value ?? ''), { timeout: ACTION_TIMEOUT });
|
|
65
|
+
return 'ok';
|
|
66
|
+
case 'check_control':
|
|
67
|
+
if (input.checked === false)
|
|
68
|
+
await loc.uncheck({ timeout: ACTION_TIMEOUT });
|
|
69
|
+
else
|
|
70
|
+
await loc.check({ timeout: ACTION_TIMEOUT });
|
|
71
|
+
return 'ok';
|
|
72
|
+
case 'assert_visible': {
|
|
73
|
+
const visible = await loc.first().isVisible();
|
|
74
|
+
if (!visible)
|
|
75
|
+
throw new Error(`${describe(input)} not visible`);
|
|
76
|
+
return 'ok';
|
|
77
|
+
}
|
|
78
|
+
default:
|
|
79
|
+
return 'skipped';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Replay a flow's grounded steps on a given page (injected, so it's testable
|
|
83
|
+
* without a real browser). Navigates to `devUrl` first for a consistent start,
|
|
84
|
+
* then runs each grounded action, stopping at the first failure. */
|
|
85
|
+
export async function replayOnPage(page, devUrl, steps) {
|
|
86
|
+
const failures = [];
|
|
87
|
+
let ran = 0;
|
|
88
|
+
const total = steps.filter((s) => ACTUATION_TOOLS.has(bare(s.tool))).length;
|
|
89
|
+
try {
|
|
90
|
+
await page.goto(devUrl, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// SPA / already on the origin — replay anyway.
|
|
94
|
+
}
|
|
95
|
+
for (let i = 0; i < steps.length; i++) {
|
|
96
|
+
if (!ACTUATION_TOOLS.has(bare(steps[i].tool)))
|
|
97
|
+
continue;
|
|
98
|
+
try {
|
|
99
|
+
if ((await applyGroundedStep(page, steps[i])) === 'ok')
|
|
100
|
+
ran++;
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
failures.push({ index: i, tool: bare(steps[i].tool), target: describe((steps[i].input ?? {})), error: errLine(e) });
|
|
104
|
+
break; // a broken step breaks the flow — stop here
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { ok: failures.length === 0, ran, total, failures };
|
|
108
|
+
}
|
|
109
|
+
/** Pick the page on the dev origin, else the foreground page. */
|
|
110
|
+
async function pickPage(browser, devUrl) {
|
|
111
|
+
const wantOrigin = originOf(devUrl);
|
|
112
|
+
const pages = browser.contexts().flatMap((c) => c.pages());
|
|
113
|
+
if (pages.length === 0)
|
|
114
|
+
return null;
|
|
115
|
+
const matches = wantOrigin ? pages.filter((p) => originOf(p.url()) === wantOrigin) : [];
|
|
116
|
+
const candidates = matches.length ? matches : pages;
|
|
117
|
+
let chosen = candidates[candidates.length - 1];
|
|
118
|
+
for (const p of candidates) {
|
|
119
|
+
try {
|
|
120
|
+
if (await p.evaluate(() => document.visibilityState === 'visible')) {
|
|
121
|
+
chosen = p;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
/* busy/closed */
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return chosen;
|
|
130
|
+
}
|
|
131
|
+
/** Connect to the debug Chrome over CDP and replay a flow's grounded steps. */
|
|
132
|
+
export async function replayGroundedSteps(opts) {
|
|
133
|
+
let browser;
|
|
134
|
+
try {
|
|
135
|
+
browser = await chromium.connectOverCDP(opts.cdpUrl, { timeout: 5000 });
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
return { ok: false, ran: 0, total: 0, failures: [{ index: -1, tool: 'connect', target: opts.cdpUrl, error: errLine(e) }] };
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const page = await pickPage(browser, opts.devUrl);
|
|
142
|
+
if (!page) {
|
|
143
|
+
return { ok: false, ran: 0, total: 0, failures: [{ index: -1, tool: 'page', target: opts.devUrl, error: 'no page on the dev origin' }] };
|
|
144
|
+
}
|
|
145
|
+
return await replayOnPage(page, opts.devUrl, opts.steps);
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
try {
|
|
149
|
+
await browser.close();
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
/* disconnect only — never kill the user's debug Chrome */
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-heal Stage 1 — the failure → heal-hint bridge.
|
|
3
|
+
*
|
|
4
|
+
* Playwright's JSON reporter records pass/fail but not *which locator* failed.
|
|
5
|
+
* Self-healing needs that to know what to re-locate. The locator IS in the error
|
|
6
|
+
* message (e.g. `waiting for getByRole('button', { name: 'Submit' })`), so this
|
|
7
|
+
* module parses the run JSON's failed tests and pulls out, per failure, the
|
|
8
|
+
* `getBy…` expression + the action that failed — the hint the heal session
|
|
9
|
+
* (Stage 2) drives the agent with.
|
|
10
|
+
*
|
|
11
|
+
* Pure + total: bad/partial JSON yields []. No FS, no agent — just parsing.
|
|
12
|
+
*/
|
|
13
|
+
export interface RunFailure {
|
|
14
|
+
/** Spec file as Playwright reports it (path or basename). */
|
|
15
|
+
specFile: string;
|
|
16
|
+
/** Test title. */
|
|
17
|
+
title: string;
|
|
18
|
+
/** First line of the error message (the human-readable failure). */
|
|
19
|
+
error: string;
|
|
20
|
+
/** The `getBy…` locator expression parsed from the error — the thing to
|
|
21
|
+
* re-locate — or undefined if the failure wasn't a locator miss. */
|
|
22
|
+
failingLocator?: string;
|
|
23
|
+
/** The Playwright action that failed: 'click' / 'fill' / 'assert' / … */
|
|
24
|
+
failingAction?: string;
|
|
25
|
+
}
|
|
26
|
+
/** The `getBy…` expression in an error message, if any. */
|
|
27
|
+
export declare function extractLocator(message: string): string | undefined;
|
|
28
|
+
/** The failing action (click / fill / assert / …) inferred from the message. */
|
|
29
|
+
export declare function extractAction(message: string): string | undefined;
|
|
30
|
+
/** Parse a Playwright JSON-reporter run into its failures (with the failing
|
|
31
|
+
* locator + action pulled from each error). Accepts the parsed object or a
|
|
32
|
+
* JSON string; anything malformed yields []. */
|
|
33
|
+
export declare function parseRunFailures(json: unknown): RunFailure[];
|
|
34
|
+
//# sourceMappingURL=runFailures.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runFailures.d.ts","sourceRoot":"","sources":["../../src/specs/runFailures.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,UAAU;IACzB,6DAA6D;IAC7D,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC;IACd;yEACqE;IACrE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yEAAyE;IACzE,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAgBD,2DAA2D;AAC3D,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGlE;AAED,gFAAgF;AAChF,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOjE;AAoCD;;iDAEiD;AACjD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,UAAU,EAAE,CAY5D"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-heal Stage 1 — the failure → heal-hint bridge.
|
|
3
|
+
*
|
|
4
|
+
* Playwright's JSON reporter records pass/fail but not *which locator* failed.
|
|
5
|
+
* Self-healing needs that to know what to re-locate. The locator IS in the error
|
|
6
|
+
* message (e.g. `waiting for getByRole('button', { name: 'Submit' })`), so this
|
|
7
|
+
* module parses the run JSON's failed tests and pulls out, per failure, the
|
|
8
|
+
* `getBy…` expression + the action that failed — the hint the heal session
|
|
9
|
+
* (Stage 2) drives the agent with.
|
|
10
|
+
*
|
|
11
|
+
* Pure + total: bad/partial JSON yields []. No FS, no agent — just parsing.
|
|
12
|
+
*/
|
|
13
|
+
/** A `getBy…(...)` call with an optional `.first()/.last()/.nth()` tail. The
|
|
14
|
+
* inner `(?:[^()]|\([^()]*\))*` tolerates one level of nested parens so
|
|
15
|
+
* `getByRole('button', { name: 'x' })` matches whole. */
|
|
16
|
+
const LOCATOR_RE = /getBy\w+\((?:[^()]|\([^()]*\))*\)(?:\.(?:first|last|nth)\([^)]*\))?/;
|
|
17
|
+
/** A KNOWN Playwright interaction at the head of an error (`locator.click: …`),
|
|
18
|
+
* restricted to real actions so a generic `Error:` / `Some:` prefix is NOT
|
|
19
|
+
* mistaken for one. */
|
|
20
|
+
const ACTION_RE = /^(?:locator\.)?(click|dblclick|fill|type|press|check|uncheck|selectOption|setInputFiles|hover|tap|focus|clear)\b/;
|
|
21
|
+
const ASSERT_RE = /\b(?:toBeVisible|toHaveText|toContainText|toHaveCount|toHaveValue|toBeChecked|toBeEnabled)\b|^expect\(/;
|
|
22
|
+
function firstLine(s) {
|
|
23
|
+
return (s.split('\n').find(l => l.trim()) ?? '').trim();
|
|
24
|
+
}
|
|
25
|
+
/** The `getBy…` expression in an error message, if any. */
|
|
26
|
+
export function extractLocator(message) {
|
|
27
|
+
const m = message.match(LOCATOR_RE);
|
|
28
|
+
return m ? m[0] : undefined;
|
|
29
|
+
}
|
|
30
|
+
/** The failing action (click / fill / assert / …) inferred from the message. */
|
|
31
|
+
export function extractAction(message) {
|
|
32
|
+
const head = firstLine(message);
|
|
33
|
+
// Assert first — an `Error: expect(...)` line leads with "Error:", which a
|
|
34
|
+
// generic action match would wrongly read as the action.
|
|
35
|
+
if (ASSERT_RE.test(head) || ASSERT_RE.test(message))
|
|
36
|
+
return 'assert';
|
|
37
|
+
const m = head.match(ACTION_RE);
|
|
38
|
+
return m ? m[1].toLowerCase() : undefined;
|
|
39
|
+
}
|
|
40
|
+
/** The error message of the first failed/timed-out result on a spec, if any. */
|
|
41
|
+
function failedMessage(spec) {
|
|
42
|
+
for (const t of spec.tests ?? []) {
|
|
43
|
+
for (const r of t.results ?? []) {
|
|
44
|
+
if (r.status === 'failed' || r.status === 'timedOut' || r.status === 'interrupted') {
|
|
45
|
+
return r.error?.message || r.errors?.find(e => e.message)?.message || 'Test failed (no error message).';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
function walk(suite, fileFallback, out) {
|
|
52
|
+
const file = suite.file || fileFallback;
|
|
53
|
+
for (const spec of suite.specs ?? []) {
|
|
54
|
+
if (spec.ok === true)
|
|
55
|
+
continue;
|
|
56
|
+
const message = failedMessage(spec);
|
|
57
|
+
if (!message)
|
|
58
|
+
continue; // not a failure (ok may be undefined but no failed result)
|
|
59
|
+
out.push({
|
|
60
|
+
specFile: spec.file || file,
|
|
61
|
+
title: spec.title || '(untitled)',
|
|
62
|
+
error: firstLine(message),
|
|
63
|
+
failingLocator: extractLocator(message),
|
|
64
|
+
failingAction: extractAction(message),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
for (const child of suite.suites ?? [])
|
|
68
|
+
walk(child, file, out);
|
|
69
|
+
}
|
|
70
|
+
/** Parse a Playwright JSON-reporter run into its failures (with the failing
|
|
71
|
+
* locator + action pulled from each error). Accepts the parsed object or a
|
|
72
|
+
* JSON string; anything malformed yields []. */
|
|
73
|
+
export function parseRunFailures(json) {
|
|
74
|
+
let root;
|
|
75
|
+
if (typeof json === 'string') {
|
|
76
|
+
try {
|
|
77
|
+
root = JSON.parse(json);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else if (json && typeof json === 'object') {
|
|
84
|
+
root = json;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
const out = [];
|
|
90
|
+
for (const suite of root.suites ?? [])
|
|
91
|
+
walk(suite, suite.file || '', out);
|
|
92
|
+
return out;
|
|
93
|
+
}
|
package/dist/specs/seeds.d.ts
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translation seeds: human-written worked examples that teach the optimization
|
|
3
|
+
* pass (F7) a multi-step Playwright pattern by few-shot — NOT by deterministic
|
|
4
|
+
* match+template. A seed is a rough `signature` (tool names, used only to pick
|
|
5
|
+
* relevant seeds) + a concrete `example` (input steps → output code) the LLM
|
|
6
|
+
* generalizes from.
|
|
7
|
+
*
|
|
8
|
+
* These ship inlined as the `BUILTIN_SEEDS` constant below. They used to be
|
|
9
|
+
* JSON files under `packages/core/seeds/optimization/` plus a `.hover/rules/`
|
|
10
|
+
* "author your own seed" mechanism and a `.hover/seeds.json` opt-out — all
|
|
11
|
+
* removed: that user-facing surface added burden for a small curated catalogue
|
|
12
|
+
* that feeds an optional, manually-invoked pass. To add a pattern, append a
|
|
13
|
+
* `SeedRule` here.
|
|
14
|
+
*/
|
|
1
15
|
export interface SeedRule {
|
|
2
16
|
/** Identifier, e.g. `download`. */
|
|
3
17
|
name: string;
|
|
@@ -13,23 +27,10 @@ export interface SeedRule {
|
|
|
13
27
|
};
|
|
14
28
|
}
|
|
15
29
|
/**
|
|
16
|
-
* Built-in seeds
|
|
17
|
-
* the
|
|
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.
|
|
30
|
+
* Built-in optimization seeds, inlined. They feed EVERY project's optimization
|
|
31
|
+
* pass (the prompt builder and the relevance filter consume this directly).
|
|
28
32
|
*/
|
|
29
33
|
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
34
|
/** Pick seeds whose signature's base tool appears in the spec — a cheap
|
|
34
35
|
* relevance filter so the prompt only carries plausibly-applicable examples. */
|
|
35
36
|
export declare function relevantSeeds(seeds: SeedRule[], specTools: Set<string>, cap?: number): SeedRule[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seeds.d.ts","sourceRoot":"","sources":["../../src/specs/seeds.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"seeds.d.ts","sourceRoot":"","sources":["../../src/specs/seeds.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,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;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,QAAQ,EAiFnC,CAAC;AAEF;iFACiF;AACjF,wBAAgB,aAAa,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,GAAG,SAAI,GAAG,QAAQ,EAAE,CAG5F"}
|
package/dist/specs/seeds.js
CHANGED
|
@@ -1,71 +1,103 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* Translation seeds: human-written worked examples that teach the optimization
|
|
3
|
+
* pass (F7) a multi-step Playwright pattern by few-shot — NOT by deterministic
|
|
4
|
+
* match+template. A seed is a rough `signature` (tool names, used only to pick
|
|
5
|
+
* relevant seeds) + a concrete `example` (input steps → output code) the LLM
|
|
6
|
+
* generalizes from.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* These ship inlined as the `BUILTIN_SEEDS` constant below. They used to be
|
|
9
|
+
* JSON files under `packages/core/seeds/optimization/` plus a `.hover/rules/`
|
|
10
|
+
* "author your own seed" mechanism and a `.hover/seeds.json` opt-out — all
|
|
11
|
+
* removed: that user-facing surface added burden for a small curated catalogue
|
|
12
|
+
* that feeds an optional, manually-invoked pass. To add a pattern, append a
|
|
13
|
+
* `SeedRule` here.
|
|
13
14
|
*/
|
|
14
|
-
import { readFile, readdir } from 'node:fs/promises';
|
|
15
|
-
import { join } from 'node:path';
|
|
16
15
|
/**
|
|
17
|
-
* Built-in seeds
|
|
18
|
-
* the
|
|
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.
|
|
16
|
+
* Built-in optimization seeds, inlined. They feed EVERY project's optimization
|
|
17
|
+
* pass (the prompt builder and the relevance filter consume this directly).
|
|
29
18
|
*/
|
|
30
19
|
export const BUILTIN_SEEDS = [
|
|
31
20
|
{
|
|
32
21
|
name: 'download',
|
|
33
22
|
signature: ['browser_click'],
|
|
34
|
-
note: '
|
|
23
|
+
note: 'A click that triggers a file download — pair it with waitForEvent(\'download\') so the listener is registered before the click fires.',
|
|
35
24
|
example: {
|
|
36
25
|
steps: [{ tool: 'browser_click', element: 'Export CSV button' }],
|
|
37
|
-
code:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
26
|
+
code: `const [download] = await Promise.all([
|
|
27
|
+
page.waitForEvent('download'),
|
|
28
|
+
page.getByRole('button', { name: 'Export CSV' }).click(),
|
|
29
|
+
]);
|
|
30
|
+
expect(await download.suggestedFilename()).toContain('.csv');`,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'file-upload',
|
|
35
|
+
signature: ['browser_file_upload'],
|
|
36
|
+
note: 'Set a file on a (often hidden) <input type=file>. The file chooser opens synchronously on click, so register waitForEvent(\'filechooser\') before the click — same race as download.',
|
|
37
|
+
example: {
|
|
38
|
+
steps: [
|
|
39
|
+
{ tool: 'browser_click', element: 'Upload avatar button' },
|
|
40
|
+
{ tool: 'browser_file_upload', paths: ['avatar.png'] },
|
|
41
|
+
],
|
|
42
|
+
code: `const [chooser] = await Promise.all([
|
|
43
|
+
page.waitForEvent('filechooser'),
|
|
44
|
+
page.getByRole('button', { name: 'Upload avatar' }).click(),
|
|
45
|
+
]);
|
|
46
|
+
await chooser.setFiles('tests/fixtures/avatar.png');
|
|
47
|
+
await expect(page.getByText('avatar.png')).toBeVisible();`,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'dialog',
|
|
52
|
+
signature: ['browser_handle_dialog'],
|
|
53
|
+
note: 'A click that triggers a native dialog (alert/confirm/prompt). Register the page \'dialog\' handler BEFORE the click that fires it — otherwise Playwright auto-dismisses it and the assertion is wrong.',
|
|
54
|
+
example: {
|
|
55
|
+
steps: [
|
|
56
|
+
{ tool: 'browser_click', element: 'Delete account button' },
|
|
57
|
+
{ tool: 'browser_handle_dialog', action: 'accept' },
|
|
58
|
+
],
|
|
59
|
+
code: `page.once('dialog', dialog => dialog.accept());
|
|
60
|
+
await page.getByRole('button', { name: 'Delete account' }).click();
|
|
61
|
+
await expect(page.getByText('Account deleted')).toBeVisible();`,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'oauth-popup',
|
|
66
|
+
signature: ['browser_click', 'browser_tabs:select'],
|
|
67
|
+
note: 'Sign in through a provider popup that opens a new tab. Pair the opener click with context.waitForEvent(\'page\'), then drive the returned popup page.',
|
|
68
|
+
example: {
|
|
69
|
+
steps: [
|
|
70
|
+
{ tool: 'browser_click', element: 'Sign in with Google button' },
|
|
71
|
+
{ tool: 'browser_tabs', action: 'select', idx: 1 },
|
|
72
|
+
],
|
|
73
|
+
code: `const [popup] = await Promise.all([
|
|
74
|
+
context.waitForEvent('page'),
|
|
75
|
+
page.getByRole('button', { name: 'Sign in with Google' }).click(),
|
|
76
|
+
]);
|
|
77
|
+
await popup.getByLabel('Email').fill('user@example.com');
|
|
78
|
+
await popup.getByRole('button', { name: 'Next' }).click();
|
|
79
|
+
await popup.waitForEvent('close');
|
|
80
|
+
await expect(page.getByText('Signed in')).toBeVisible();`,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'network-gated-assertion',
|
|
85
|
+
signature: ['browser_click', 'browser_wait_for'],
|
|
86
|
+
note: 'A click fires an XHR/fetch and the result is asserted. Pair the click with page.waitForResponse so the test waits for the real request to settle, instead of a guessed timeout or a race.',
|
|
87
|
+
example: {
|
|
88
|
+
steps: [
|
|
89
|
+
{ tool: 'browser_click', element: 'Place order button' },
|
|
90
|
+
{ tool: 'browser_wait_for', text: 'Order confirmed' },
|
|
91
|
+
],
|
|
92
|
+
code: `const [res] = await Promise.all([
|
|
93
|
+
page.waitForResponse(r => r.url().includes('/api/orders') && r.request().method() === 'POST'),
|
|
94
|
+
page.getByRole('button', { name: 'Place order' }).click(),
|
|
95
|
+
]);
|
|
96
|
+
expect(res.ok()).toBeTruthy();
|
|
97
|
+
await expect(page.getByText('Order confirmed')).toBeVisible();`,
|
|
42
98
|
},
|
|
43
99
|
},
|
|
44
100
|
];
|
|
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
101
|
/** Pick seeds whose signature's base tool appears in the spec — a cheap
|
|
70
102
|
* relevance filter so the prompt only carries plausibly-applicable examples. */
|
|
71
103
|
export function relevantSeeds(seeds, specTools, cap = 6) {
|
package/dist/specs/sidecar.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SkillStep } from '../
|
|
1
|
+
import type { SkillStep } from '../specs/specStep.js';
|
|
2
2
|
import type { SpecAssertion } from './writeSpec.js';
|
|
3
3
|
/** Current sidecar schema version. Bump when the shape changes so readers
|
|
4
4
|
* (Stage 2 detection, Stage 7 optimization) can migrate or skip cleanly. */
|
|
@@ -15,11 +15,39 @@ export interface SpecSidecar {
|
|
|
15
15
|
/** Alt-click assertions captured alongside the session. */
|
|
16
16
|
assertions: SpecAssertion[];
|
|
17
17
|
}
|
|
18
|
-
/**
|
|
19
|
-
*
|
|
18
|
+
/** Project-root `.hover/` directory — the single home for Hover-derived data
|
|
19
|
+
* (sidecars, runs, rules, conventions). */
|
|
20
|
+
export declare function hoverDir(devRoot: string): string;
|
|
21
|
+
/** Sanitize an id segment for use as a directory name (conversation / run id). */
|
|
22
|
+
export declare function safeSeg(s: string): string;
|
|
23
|
+
/** Per-run home: `<devRoot>/.hover/conversations/<conversationId>/<runId>/`.
|
|
24
|
+
* Everything a single agent run produces — meta.json (the ledger record),
|
|
25
|
+
* report.md (QA), screenshots/ — lives here, grouped under its conversation so
|
|
26
|
+
* deleting a conversation is one `rm -rf .hover/conversations/<conversationId>`.
|
|
27
|
+
* (Distinct from `.hover/runs/`, which is the Playwright spec-run-results
|
|
28
|
+
* ledger written by ▶ Run.) */
|
|
29
|
+
export declare function conversationsDir(devRoot: string): string;
|
|
30
|
+
export declare function conversationDir(devRoot: string, conversationId: string): string;
|
|
31
|
+
export declare function runDir(devRoot: string, conversationId: string, runId: string): string;
|
|
32
|
+
/** Sidecar directory: `<devRoot>/.hover/sidecars`. Outside `__vibe_tests__/`,
|
|
33
|
+
* so Playwright's default `*.spec.ts` glob trivially never reaches it. */
|
|
20
34
|
export declare function sidecarDir(devRoot: string): string;
|
|
21
|
-
/**
|
|
22
|
-
*
|
|
23
|
-
|
|
35
|
+
/** Pre-relocation sidecar home (`__vibe_tests__/.hover/`). Read-only fallback;
|
|
36
|
+
* nothing writes here anymore. */
|
|
37
|
+
export declare function legacySidecarDir(devRoot: string): string;
|
|
38
|
+
/** Write the structured-session sidecar at `.hover/sidecars/<slug>.json`.
|
|
39
|
+
* Caller passes the data minus the stamped fields (`version`, `createdAt`),
|
|
40
|
+
* which this function fills. Returns the absolute path written. */
|
|
24
41
|
export declare function writeSidecar(devRoot: string, data: Omit<SpecSidecar, 'version' | 'createdAt'>): Promise<string>;
|
|
42
|
+
/**
|
|
43
|
+
* Read one sidecar by slug, with legacy fallback + lazy copy-forward: when a
|
|
44
|
+
* sidecar only exists at the pre-relocation `__vibe_tests__/.hover/` path it
|
|
45
|
+
* is parsed from there and best-effort re-written into `.hover/sidecars/` so
|
|
46
|
+
* the next read hits the new home. Returns `null` when absent or malformed.
|
|
47
|
+
*/
|
|
48
|
+
export declare function readSidecar(devRoot: string, slug: string): Promise<SpecSidecar | null>;
|
|
49
|
+
/** Parse one sidecar file, or `null` when absent / not JSON. Deliberately
|
|
50
|
+
* lenient on shape (an empty `{}` still counts as "a sidecar exists") —
|
|
51
|
+
* consumers that need `steps`/`slug` filter for themselves. */
|
|
52
|
+
export declare function parseSidecarFile(path: string): Promise<SpecSidecar | null>;
|
|
25
53
|
//# sourceMappingURL=sidecar.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar.d.ts","sourceRoot":"","sources":["../../src/specs/sidecar.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sidecar.d.ts","sourceRoot":"","sources":["../../src/specs/sidecar.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,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;4CAC4C;AAC5C,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,kFAAkF;AAClF,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEzC;AAED;;;;;gCAKgC;AAChC,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD;AACD,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM,CAE/E;AACD,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAErF;AAED;2EAC2E;AAC3E,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAElD;AAED;mCACmC;AACnC,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;oEAEoE;AACpE,wBAAsB,YAAY,CAChC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,SAAS,GAAG,WAAW,CAAC,GAC/C,OAAO,CAAC,MAAM,CAAC,CAWjB;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAoB5F;AAED;;gEAEgE;AAChE,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAOhF"}
|