@hover-dev/core 0.15.0 → 0.16.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 (49) hide show
  1. package/dist/agents/aider.d.ts.map +1 -1
  2. package/dist/agents/aider.js +6 -14
  3. package/dist/agents/codex.d.ts.map +1 -1
  4. package/dist/agents/codex.js +9 -4
  5. package/dist/agents/cursor.d.ts.map +1 -1
  6. package/dist/agents/cursor.js +8 -17
  7. package/dist/agents/gemini.d.ts.map +1 -1
  8. package/dist/agents/gemini.js +3 -14
  9. package/dist/agents/qwen.d.ts.map +1 -1
  10. package/dist/agents/qwen.js +3 -14
  11. package/dist/agents/shared.d.ts +28 -0
  12. package/dist/agents/shared.d.ts.map +1 -0
  13. package/dist/agents/shared.js +35 -0
  14. package/dist/mcp/sourceFence.d.ts +23 -0
  15. package/dist/mcp/sourceFence.d.ts.map +1 -0
  16. package/dist/mcp/sourceFence.js +75 -0
  17. package/dist/mcp/sourceServer.d.ts +3 -0
  18. package/dist/mcp/sourceServer.d.ts.map +1 -0
  19. package/dist/mcp/sourceServer.js +116 -0
  20. package/dist/playwright/preflight.d.ts.map +1 -1
  21. package/dist/playwright/preflight.js +6 -1
  22. package/dist/playwright/raiseWindow.d.ts.map +1 -1
  23. package/dist/playwright/raiseWindow.js +22 -3
  24. package/dist/playwright/resolveMcpConfig.d.ts +6 -0
  25. package/dist/playwright/resolveMcpConfig.d.ts.map +1 -1
  26. package/dist/playwright/resolveMcpConfig.js +15 -2
  27. package/dist/plugin-api.d.ts +7 -0
  28. package/dist/plugin-api.d.ts.map +1 -1
  29. package/dist/runSession.d.ts.map +1 -1
  30. package/dist/runSession.js +5 -0
  31. package/dist/service/cdpHandlers.d.ts +3 -7
  32. package/dist/service/cdpHandlers.d.ts.map +1 -1
  33. package/dist/service/cdpHandlers.js +4 -16
  34. package/dist/service.d.ts +6 -0
  35. package/dist/service.d.ts.map +1 -1
  36. package/dist/service.js +128 -49
  37. package/dist/specs/optimizeSpec.d.ts.map +1 -1
  38. package/dist/specs/optimizeSpec.js +28 -6
  39. package/dist/specs/softBatch.d.ts +14 -0
  40. package/dist/specs/softBatch.d.ts.map +1 -0
  41. package/dist/specs/softBatch.js +177 -0
  42. package/dist/specs/text.d.ts +17 -0
  43. package/dist/specs/text.d.ts.map +1 -0
  44. package/dist/specs/text.js +24 -0
  45. package/dist/specs/writeCaseCsv.d.ts.map +1 -1
  46. package/dist/specs/writeCaseCsv.js +2 -8
  47. package/dist/specs/writeSpec.d.ts.map +1 -1
  48. package/dist/specs/writeSpec.js +2 -9
  49. package/package.json +5 -2
@@ -0,0 +1,177 @@
1
+ /**
2
+ * soft-batch — a deterministic finishing step of the optimization pass.
3
+ *
4
+ * After the LLM optimize pass decides WHAT to assert (it reads the observed
5
+ * feedback and adds the trailing "Then" assertions), this step applies the safe
6
+ * mechanical rewrite: the maximal *trailing run* of independent assertions in
7
+ * each `test()` body goes from `expect(...)` to `expect.soft(...)`, so a
8
+ * field-audit spec reports every failing field in one run instead of halting on
9
+ * the first. This is the "LLM decides, AST executes" split — the LLM never
10
+ * reprints the file to do it, so it can't drift; ts-morph applies the soften
11
+ * surgically and every untouched node stays byte-identical.
12
+ *
13
+ * Two assertion shapes are recognised, because that is what specs look like
14
+ * after optimize:
15
+ * A. a bare `await expect(...)....` statement;
16
+ * B. a Hover `await test.step('Then · …', async () => { await expect(...) })`
17
+ * closure whose body is nothing but assertions — Hover emits one such step
18
+ * per observed-feedback assertion, AFTER all the action steps.
19
+ *
20
+ * Safety guard (why this is deterministic and never changes test semantics):
21
+ * we only ever soften an assertion that sits in the trailing run — the suffix
22
+ * of the test body that is assertions all the way down, with no action after
23
+ * it. A *gating* assertion — one a later action depends on — is by construction
24
+ * followed by that action, so it never lands in the trailing run and is never
25
+ * softened. Softening only changes how failures are *reported* (all collected
26
+ * vs. stop-on-first), never whether the test passes. We require ≥2 assertions
27
+ * in the run: soft buys nothing for a single one. `expect.soft` collects across
28
+ * the whole test regardless of `test.step` nesting, so softening step-wrapped
29
+ * assertions is sound.
30
+ */
31
+ import { Project, SyntaxKind, Node } from 'ts-morph';
32
+ /** A trailing run with fewer than this many assertions is left alone —
33
+ * `expect.soft` only earns its keep when ≥2 failures could be collected. */
34
+ export const MIN_RUN = 2;
35
+ /** Run the soft-batch step over a spec's source text. Pure: text in, text out. */
36
+ export function softBatch(source) {
37
+ const project = new Project({
38
+ useInMemoryFileSystem: true,
39
+ compilerOptions: { allowJs: true },
40
+ });
41
+ const sf = project.createSourceFile('__spec.ts', source, { overwrite: true });
42
+ // Collect every assertion to soften first (across all test bodies), then
43
+ // apply — each edit is a localized identifier replace, so sibling targets
44
+ // stay valid.
45
+ const targets = [];
46
+ for (const body of testBodies(sf)) {
47
+ const run = trailingAssertionRun(body.getStatements());
48
+ const asserts = run.flatMap(bareAssertionsIn);
49
+ if (asserts.length >= MIN_RUN)
50
+ targets.push(...asserts);
51
+ }
52
+ let softened = 0;
53
+ for (const stmt of targets) {
54
+ if (soften(stmt))
55
+ softened++;
56
+ }
57
+ return { code: sf.getFullText(), changed: softened > 0, softened };
58
+ }
59
+ /**
60
+ * Yield the block body of every `test(...)` call (including `test.only` /
61
+ * `.skip` / `.fixme`), but NOT `test.describe` / hooks — those wrap tests, they
62
+ * aren't a test body.
63
+ */
64
+ function testBodies(sf) {
65
+ const bodies = [];
66
+ for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
67
+ if (!isTestCall(call.getExpression()))
68
+ continue;
69
+ const cb = call.getArguments().at(-1);
70
+ if (!cb)
71
+ continue;
72
+ if (Node.isArrowFunction(cb) || Node.isFunctionExpression(cb)) {
73
+ const block = cb.getBody();
74
+ if (Node.isBlock(block))
75
+ bodies.push(block);
76
+ }
77
+ }
78
+ return bodies;
79
+ }
80
+ /** `test` | `test.only` | `test.skip` | `test.fixme` — not `test.describe` or hooks. */
81
+ function isTestCall(callee) {
82
+ if (Node.isIdentifier(callee))
83
+ return callee.getText() === 'test';
84
+ if (Node.isPropertyAccessExpression(callee)) {
85
+ const base = callee.getExpression();
86
+ if (!Node.isIdentifier(base) || base.getText() !== 'test')
87
+ return false;
88
+ return ['only', 'skip', 'fixme'].includes(callee.getName());
89
+ }
90
+ return false;
91
+ }
92
+ /** The longest suffix of `statements` that are all assertion units (a bare
93
+ * assertion, or a `test.step` whose body is only assertions). */
94
+ function trailingAssertionRun(statements) {
95
+ const run = [];
96
+ for (let i = statements.length - 1; i >= 0; i--) {
97
+ if (!isAssertionUnit(statements[i]))
98
+ break;
99
+ run.unshift(statements[i]);
100
+ }
101
+ return run;
102
+ }
103
+ /** A statement that contributes only assertions: a bare assertion (case A) or
104
+ * an assertion-only `test.step` closure (case B). */
105
+ function isAssertionUnit(stmt) {
106
+ return isBareAssertion(stmt) || bareAssertionsInStep(stmt) !== null;
107
+ }
108
+ /** The bare-assertion statements a unit contains: itself (case A) or the
109
+ * assertions inside its `test.step` closure (case B). */
110
+ function bareAssertionsIn(stmt) {
111
+ if (isBareAssertion(stmt))
112
+ return [stmt];
113
+ return bareAssertionsInStep(stmt) ?? [];
114
+ }
115
+ /** True if the statement is `(await) expect(...)....` — its call chain bottoms
116
+ * out at an `expect` identifier. */
117
+ function isBareAssertion(stmt) {
118
+ if (!Node.isExpressionStatement(stmt))
119
+ return false;
120
+ let expr = stmt.getExpression();
121
+ if (Node.isAwaitExpression(expr))
122
+ expr = expr.getExpression();
123
+ return leftmostBase(expr) === 'expect';
124
+ }
125
+ /**
126
+ * If `stmt` is `await test.step(label, async () => { …only assertions… })`,
127
+ * return those inner assertion statements; otherwise null. The closure body
128
+ * must be non-empty and contain ONLY bare assertions — a step that also acts
129
+ * (a "When" with a check) is not a pure assertion unit and is left alone.
130
+ */
131
+ function bareAssertionsInStep(stmt) {
132
+ if (!Node.isExpressionStatement(stmt))
133
+ return null;
134
+ let expr = stmt.getExpression();
135
+ if (Node.isAwaitExpression(expr))
136
+ expr = expr.getExpression();
137
+ if (!Node.isCallExpression(expr))
138
+ return null;
139
+ const callee = expr.getExpression();
140
+ if (!Node.isPropertyAccessExpression(callee) ||
141
+ callee.getName() !== 'step' ||
142
+ callee.getExpression().getText() !== 'test') {
143
+ return null;
144
+ }
145
+ const cb = expr.getArguments().at(-1);
146
+ if (!cb || !(Node.isArrowFunction(cb) || Node.isFunctionExpression(cb)))
147
+ return null;
148
+ const block = cb.getBody();
149
+ if (!Node.isBlock(block))
150
+ return null;
151
+ const inner = block.getStatements();
152
+ if (inner.length === 0 || !inner.every(isBareAssertion))
153
+ return null;
154
+ return inner;
155
+ }
156
+ /** Descend a call/member chain to its leftmost identifier, e.g.
157
+ * `expect(x).toHaveText(y)` → "expect", `page.goto('/')` → "page". */
158
+ function leftmostBase(node) {
159
+ let cur = node;
160
+ while (cur && (Node.isCallExpression(cur) || Node.isPropertyAccessExpression(cur))) {
161
+ cur = cur.getExpression();
162
+ }
163
+ return cur && Node.isIdentifier(cur) ? cur.getText() : null;
164
+ }
165
+ /** Rewrite the `expect(` call inside a bare assertion statement to
166
+ * `expect.soft(`. Skips ones already soft (their callee is `expect.soft`, not
167
+ * the bare identifier `expect`). Returns whether a change was made. */
168
+ function soften(stmt) {
169
+ for (const call of stmt.getDescendantsOfKind(SyntaxKind.CallExpression)) {
170
+ const callee = call.getExpression();
171
+ if (Node.isIdentifier(callee) && callee.getText() === 'expect') {
172
+ callee.replaceWithText('expect.soft');
173
+ return true;
174
+ }
175
+ }
176
+ return false;
177
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Small text helpers shared across the spec/CSV emitters.
3
+ *
4
+ * Hoisted here so the two crystallization outputs (writeSpec's JSDoc header and
5
+ * writeCaseCsv's Xray rows) derive a slug and a one-sentence "Expected" line the
6
+ * same way — they used to carry byte-identical copies of this logic.
7
+ */
8
+ /** Lowercase, hyphenate, and trim a display name into a filesystem-safe slug. */
9
+ export declare function slugify(name: string): string;
10
+ /**
11
+ * The first sentence of a done-summary, trimmed. Agents sometimes ramble; the
12
+ * Expected blocks only want the leading sentence. Splits on the gap that
13
+ * follows sentence-ending punctuation (`.`, `!`, `?`); a summary with no such
14
+ * punctuation comes back trimmed in full.
15
+ */
16
+ export declare function firstSentence(summary: string): string;
17
+ //# sourceMappingURL=text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../src/specs/text.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,iFAAiF;AACjF,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM5C;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAErD"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Small text helpers shared across the spec/CSV emitters.
3
+ *
4
+ * Hoisted here so the two crystallization outputs (writeSpec's JSDoc header and
5
+ * writeCaseCsv's Xray rows) derive a slug and a one-sentence "Expected" line the
6
+ * same way — they used to carry byte-identical copies of this logic.
7
+ */
8
+ /** Lowercase, hyphenate, and trim a display name into a filesystem-safe slug. */
9
+ export function slugify(name) {
10
+ return name
11
+ .toLowerCase()
12
+ .trim()
13
+ .replace(/[^a-z0-9]+/g, '-')
14
+ .replace(/^-+|-+$/g, '');
15
+ }
16
+ /**
17
+ * The first sentence of a done-summary, trimmed. Agents sometimes ramble; the
18
+ * Expected blocks only want the leading sentence. Splits on the gap that
19
+ * follows sentence-ending punctuation (`.`, `!`, `?`); a summary with no such
20
+ * punctuation comes back trimmed in full.
21
+ */
22
+ export function firstSentence(summary) {
23
+ return summary.split(/(?<=[.!?])\s+/)[0]?.trim() ?? summary.trim();
24
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"writeCaseCsv.d.ts","sourceRoot":"","sources":["../../src/specs/writeCaseCsv.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpD,qBAAa,kBAAmB,SAAQ,KAAK;aACf,IAAI,EAAE,MAAM;aAAkB,IAAI,EAAE,MAAM;gBAA1C,IAAI,EAAE,MAAM,EAAkB,IAAI,EAAE,MAAM;CAIvE;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,UAAU,CAAC,EAAE,aAAa,EAAE,CAAC;IAC7B;;uEAEmE;IACnE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;+CAC2C;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,kBAAkB;IAAG,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;CAAE;AAEnE,wBAAsB,YAAY,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAiBzF"}
1
+ {"version":3,"file":"writeCaseCsv.d.ts","sourceRoot":"","sources":["../../src/specs/writeCaseCsv.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAGpD,qBAAa,kBAAmB,SAAQ,KAAK;aACf,IAAI,EAAE,MAAM;aAAkB,IAAI,EAAE,MAAM;gBAA1C,IAAI,EAAE,MAAM,EAAkB,IAAI,EAAE,MAAM;CAIvE;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,UAAU,CAAC,EAAE,aAAa,EAAE,CAAC;IAC7B;;uEAEmE;IACnE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;+CAC2C;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,kBAAkB;IAAG,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;CAAE;AAEnE,wBAAsB,YAAY,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAiBzF"}
@@ -31,6 +31,7 @@ import { mkdir, writeFile } from 'node:fs/promises';
31
31
  import { existsSync } from 'node:fs';
32
32
  import { join } from 'node:path';
33
33
  import { humanSteps } from './humanSteps.js';
34
+ import { slugify, firstSentence } from './text.js';
34
35
  export class CaseCsvExistsError extends Error {
35
36
  slug;
36
37
  path;
@@ -59,13 +60,6 @@ export async function writeCaseCsv(opts) {
59
60
  return { path, slug };
60
61
  }
61
62
  // ───────── helpers ─────────
62
- function slugify(name) {
63
- return name
64
- .toLowerCase()
65
- .trim()
66
- .replace(/[^a-z0-9]+/g, '-')
67
- .replace(/^-+|-+$/g, '');
68
- }
69
63
  function renderCsv(slug, opts) {
70
64
  const rows = buildRows(slug, opts);
71
65
  // CRLF row terminator — what Excel / Numbers / Xray's importer all
@@ -112,7 +106,7 @@ function expectedFor(assertions, steps) {
112
106
  }
113
107
  const done = [...steps].reverse().find(s => s.kind === 'done');
114
108
  if (done?.summary) {
115
- return done.summary.split(/(?<=[.!?])\s+/)[0]?.trim() ?? done.summary.trim();
109
+ return firstSentence(done.summary);
116
110
  }
117
111
  return '';
118
112
  }
@@ -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;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"}
1
+ {"version":3,"file":"writeSpec.d.ts","sourceRoot":"","sources":["../../src/specs/writeSpec.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAWzD,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;AAuUD;;;;;;;;;;;;;;;;;;;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"}
@@ -23,6 +23,7 @@ import { humanSteps, humanStep } from './humanSteps.js';
23
23
  import { writeSidecar } from './sidecar.js';
24
24
  import { readPageObjectManifest, } from './pageObjectManifest.js';
25
25
  import { stepSignature } from './detectSharedFlows.js';
26
+ import { slugify, firstSentence } from './text.js';
26
27
  /**
27
28
  * Marker the deterministic translator leaves where a captured action is a real
28
29
  * interaction but has no single-step Playwright translation (e.g. file upload,
@@ -80,13 +81,6 @@ export async function writeSpec(opts) {
80
81
  });
81
82
  return { path, slug };
82
83
  }
83
- function slugify(name) {
84
- return name
85
- .toLowerCase()
86
- .trim()
87
- .replace(/[^a-z0-9]+/g, '-')
88
- .replace(/^-+|-+$/g, '');
89
- }
90
84
  // Escape sequences that would prematurely terminate the JSDoc block.
91
85
  // (Backtick literal of close-comment sequence omitted on purpose — see how
92
86
  // the regex below is built — to avoid recursively poisoning *this* file.)
@@ -108,8 +102,7 @@ function collectExpected(assertions, doneSummary) {
108
102
  }
109
103
  if (doneSummary && doneSummary.trim()) {
110
104
  // Take the first sentence only — agents sometimes ramble.
111
- const first = doneSummary.split(/(?<=[.!?])\s+/)[0] ?? doneSummary;
112
- return [first.trim()];
105
+ return [firstSentence(doneSummary)];
113
106
  }
114
107
  return [];
115
108
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hover-dev/core",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Hover's local Node service: agent invocation, Playwright CDP preflight, WebSocket bridge.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hyperyond",
@@ -49,10 +49,13 @@
49
49
  "README.md"
50
50
  ],
51
51
  "dependencies": {
52
+ "@modelcontextprotocol/sdk": "^1.29.0",
52
53
  "@playwright/mcp": "0.0.75",
53
54
  "cross-spawn": "^7.0.6",
54
55
  "playwright-core": "^1.50.0",
55
- "ws": "^8.20.1"
56
+ "ts-morph": "^28.0.0",
57
+ "ws": "^8.20.1",
58
+ "zod": "^4.4.3"
56
59
  },
57
60
  "devDependencies": {
58
61
  "@types/cross-spawn": "^6.0.6",