@hover-dev/core 0.23.0 → 0.25.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/dist/engine.d.ts CHANGED
@@ -18,8 +18,10 @@ export type { SkillStep } from './specs/specStep.js';
18
18
  export { reRenderSpec } from './specs/writeSpec.js';
19
19
  export { writeApiSpec } from './specs/writeApiSpec.js';
20
20
  export type { ApiCheck, WriteApiSpecOptions, WriteApiSpecResult } from './specs/writeApiSpec.js';
21
- export { buildOptimizeBrief, saveOptimizedCandidate, OptimizeError } from './specs/optimizeSpec.js';
21
+ export { buildOptimizeBrief, saveOptimizedCandidate, promoteOptimizedCandidate, OptimizeError } from './specs/optimizeSpec.js';
22
22
  export type { OptimizeResult } from './specs/optimizeSpec.js';
23
+ export { appendWikiLog, readWikiLog, wikiLogPath } from './specs/wikiLog.js';
24
+ export type { WikiLogKind, WikiLogEntry } from './specs/wikiLog.js';
23
25
  export { lintWiki, parseRunStatuses } from './specs/lintWiki.js';
24
26
  export type { LintResult, LintFinding, LintKind, LintSeverity } from './specs/lintWiki.js';
25
27
  export { parseBusinessMap } from './specs/businessMap.js';
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACzF,YAAY,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEpD,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,YAAY,EAAE,QAAQ,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAGjG,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACpG,YAAY,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAE9D,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACjE,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAC3F,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,YAAY,EAAE,gBAAgB,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAEjF,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAC3F,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAClF,YAAY,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAE/D,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AACjH,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAEzG,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAGtD,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACrG,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAKhF,OAAO,EAAE,UAAU,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAC5J,YAAY,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAG/D,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACzG,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACzF,YAAY,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEpD,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,YAAY,EAAE,QAAQ,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAGjG,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,yBAAyB,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC/H,YAAY,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAE9D,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAC7E,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEpE,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACjE,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAC3F,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,YAAY,EAAE,gBAAgB,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAEjF,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAC3F,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAClF,YAAY,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAE/D,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AACjH,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAEzG,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAGtD,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACrG,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAKhF,OAAO,EAAE,UAAU,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAC5J,YAAY,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAG/D,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACzG,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/engine.js CHANGED
@@ -19,7 +19,9 @@ export { reRenderSpec } from './specs/writeSpec.js';
19
19
  export { writeApiSpec } from './specs/writeApiSpec.js';
20
20
  // Optimize (F7) — build the improvement brief for the user's own agent, then
21
21
  // file its result as a reviewed candidate. No Hover-owned model runs.
22
- export { buildOptimizeBrief, saveOptimizedCandidate, OptimizeError } from './specs/optimizeSpec.js';
22
+ export { buildOptimizeBrief, saveOptimizedCandidate, promoteOptimizedCandidate, OptimizeError } from './specs/optimizeSpec.js';
23
+ // LLM-Wiki P3 log — append-only, machine-parseable run history at .hover/log.md.
24
+ export { appendWikiLog, readWikiLog, wikiLogPath } from './specs/wikiLog.js';
23
25
  // LLM-Wiki P1 Lint — deterministic health check over .hover/ (map vs specs vs runs).
24
26
  export { lintWiki, parseRunStatuses } from './specs/lintWiki.js';
25
27
  export { parseBusinessMap } from './specs/businessMap.js';
@@ -1,5 +1,8 @@
1
1
  export type MapNodeKind = 'app' | 'area' | 'line' | 'spec';
2
2
  export type CoverageStatus = 'covered' | 'uncovered';
3
+ /** Inter-line relationship kinds recorded in the map's `## Relationships` block
4
+ * (LLM-Wiki P2) — the graph edges that aren't the app→area→line→spec hierarchy. */
5
+ export type RelationKind = 'depends-on' | 'shares-state' | 'navigates-to';
3
6
  export interface MapNode {
4
7
  id: string;
5
8
  label: string;
@@ -12,10 +15,18 @@ export interface MapEdge {
12
15
  source: string;
13
16
  target: string;
14
17
  }
18
+ /** A resolved inter-line edge: source/target are `line:` node ids. */
19
+ export interface MapRelation {
20
+ source: string;
21
+ target: string;
22
+ kind: RelationKind;
23
+ }
15
24
  export interface BusinessMapGraph {
16
25
  app: string;
17
26
  nodes: MapNode[];
18
27
  edges: MapEdge[];
28
+ /** Inter-line relationships from the `## Relationships` block (may be empty). */
29
+ relations: MapRelation[];
19
30
  stats: {
20
31
  lines: number;
21
32
  covered: number;
@@ -1 +1 @@
1
- {"version":3,"file":"businessMap.d.ts","sourceRoot":"","sources":["../../src/specs/businessMap.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAC3D,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,WAAW,CAAC;AAErD,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AACD,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AACD,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1D;AA4BD,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,WAAW,SAAQ,GAAG,gBAAgB,CAuDlF"}
1
+ {"version":3,"file":"businessMap.d.ts","sourceRoot":"","sources":["../../src/specs/businessMap.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAC3D,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,WAAW,CAAC;AACrD;oFACoF;AACpF,MAAM,MAAM,YAAY,GAAG,YAAY,GAAG,cAAc,GAAG,cAAc,CAAC;AAE1E,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AACD,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AACD,sEAAsE;AACtE,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,YAAY,CAAC;CACpB;AACD,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,iFAAiF;IACjF,SAAS,EAAE,WAAW,EAAE,CAAC;IACzB,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1D;AA+BD,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,WAAW,SAAQ,GAAG,gBAAgB,CAmFlF"}
@@ -8,6 +8,8 @@
8
8
  * copy (packages/vscode-ext/src/businessMap.ts) on purpose — a read-only view
9
9
  * must not depend on the engine — so keep the two in sync if the format changes.
10
10
  */
11
+ const RELATION_KINDS = ['depends-on', 'shares-state', 'navigates-to'];
12
+ const RELATION_RE = new RegExp(`^\\s*-\\s+(.+?)\\s+(${RELATION_KINDS.join('|')})\\s+(.+?)\\s*$`);
11
13
  function slug(s) {
12
14
  return (s
13
15
  .toLowerCase()
@@ -50,14 +52,27 @@ export function parseBusinessMap(md, fallbackApp = 'app') {
50
52
  }
51
53
  add({ id: 'app', label: app, kind: 'app' });
52
54
  let area = null;
55
+ let inRelationships = false;
53
56
  let covered = 0;
54
57
  let lineCount = 0;
55
58
  let areaCount = 0;
59
+ // name-slug → line node id, so the `## Relationships` block can resolve a line
60
+ // by its label regardless of which area it sits under. First-defined wins.
61
+ const lineBySlug = new Map();
62
+ const rawRelations = [];
56
63
  for (const raw of md.split('\n')) {
57
64
  const line = raw.trimEnd();
58
65
  const areaM = line.match(/^##\s+(.+)$/);
59
66
  if (areaM) {
60
67
  const label = areaM[1].trim();
68
+ // The Relationships block is metadata, not an area — don't node it; its
69
+ // items are edges (parsed below), not business lines.
70
+ if (slug(label) === 'relationships') {
71
+ inRelationships = true;
72
+ area = null;
73
+ continue;
74
+ }
75
+ inRelationships = false;
61
76
  const id = `area:${slug(label)}`;
62
77
  area = { id };
63
78
  add({ id, label, kind: 'area' });
@@ -65,6 +80,12 @@ export function parseBusinessMap(md, fallbackApp = 'app') {
65
80
  areaCount++;
66
81
  continue;
67
82
  }
83
+ if (inRelationships) {
84
+ const relM = line.match(RELATION_RE);
85
+ if (relM)
86
+ rawRelations.push({ source: relM[1].trim(), kind: relM[2], target: relM[3].trim() });
87
+ continue;
88
+ }
68
89
  const itemM = line.match(/^\s*-\s*\[([ xX])\]\s+(.+)$/);
69
90
  if (itemM) {
70
91
  const status = itemM[1].toLowerCase() === 'x' ? 'covered' : 'uncovered';
@@ -72,6 +93,8 @@ export function parseBusinessMap(md, fallbackApp = 'app') {
72
93
  const parentId = area?.id ?? 'app';
73
94
  const lineId = `line:${slug(area ? area.id.slice(5) : 'top')}/${slug(name)}`;
74
95
  add({ id: lineId, label: name, kind: 'line', status, route, spec });
96
+ if (!lineBySlug.has(slug(name)))
97
+ lineBySlug.set(slug(name), lineId);
75
98
  edges.push({ source: parentId, target: lineId });
76
99
  lineCount++;
77
100
  if (status === 'covered')
@@ -83,5 +106,14 @@ export function parseBusinessMap(md, fallbackApp = 'app') {
83
106
  }
84
107
  }
85
108
  }
86
- return { app, nodes, edges, stats: { lines: lineCount, covered, areas: areaCount } };
109
+ // Resolve relationships against the lines now that all are known; an edge whose
110
+ // endpoints don't both name a known line is dropped (a stale/typo'd reference).
111
+ const relations = [];
112
+ for (const r of rawRelations) {
113
+ const source = lineBySlug.get(slug(r.source));
114
+ const target = lineBySlug.get(slug(r.target));
115
+ if (source && target && source !== target)
116
+ relations.push({ source, target, kind: r.kind });
117
+ }
118
+ return { app, nodes, edges, relations, stats: { lines: lineCount, covered, areas: areaCount } };
87
119
  }
@@ -54,6 +54,16 @@ export declare function saveOptimizedCandidate(devRoot: string, slug: string, ll
54
54
  candidatePath: string;
55
55
  code: string;
56
56
  }>;
57
+ /**
58
+ * Promote a reviewed optimization candidate: overwrite the real spec with the
59
+ * `.hover/cache/optimized/<slug>.spec.ts.draft` and remove the draft. This is the
60
+ * one place a candidate replaces the original — done ONLY on the user's say-so
61
+ * (they reviewed the diff), never automatically as part of optimize. Re-validates
62
+ * the draft (in case it was hand-edited) before applying. Throws if no draft.
63
+ */
64
+ export declare function promoteOptimizedCandidate(devRoot: string, slug: string): Promise<{
65
+ path: string;
66
+ }>;
57
67
  /**
58
68
  * Build the codegen prompt: the current spec + the observed session, plus the
59
69
  * same rules the deterministic path enforces (semantic selectors, no XPath, no
@@ -1 +1 @@
1
- {"version":3,"file":"optimizeSpec.d.ts","sourceRoot":"","sources":["../../src/specs/optimizeSpec.ts"],"names":[],"mappings":"AAeA,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAC7D,OAAO,EAAgC,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGzE,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;;;4DAG4D;AAC5D,MAAM,WAAW,YAAY;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC3C;AAOD;0DAC0D;AAC1D,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAgB/E;AAED,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,CAKzB;AAED;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CA8B/C;AAED;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAkBlD;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,WAAW,GAAG,IAAI,EAC3B,KAAK,GAAE,QAAQ,EAAO,EACtB,KAAK,GAAE,YAA4B,EACnC,iBAAiB,SAA+F,GAC/G,MAAM,CAiFR;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,CAWhF"}
1
+ {"version":3,"file":"optimizeSpec.d.ts","sourceRoot":"","sources":["../../src/specs/optimizeSpec.ts"],"names":[],"mappings":"AAeA,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAC7D,OAAO,EAAgC,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGzE,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;;;4DAG4D;AAC5D,MAAM,WAAW,YAAY;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC3C;AAOD;0DAC0D;AAC1D,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAgB/E;AAED,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,CAKzB;AAED;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CA+B/C;AAED;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAkBlD;AA6BD;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAgBxG;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,WAAW,GAAG,IAAI,EAC3B,KAAK,GAAE,QAAQ,EAAO,EACtB,KAAK,GAAE,YAA4B,EACnC,iBAAiB,SAA+F,GAC/G,MAAM,CAiFR;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,CAWhF"}
@@ -10,7 +10,7 @@
10
10
  * The LLM call is injected (`runCodegen`) so callers wire their own agent and
11
11
  * tests run deterministically without spawning anything.
12
12
  */
13
- import { readFile, mkdir, writeFile, readdir } from 'node:fs/promises';
13
+ import { readFile, mkdir, writeFile, readdir, rm } from 'node:fs/promises';
14
14
  import { join } from 'node:path';
15
15
  import { Project } from 'ts-morph';
16
16
  import { readSidecar } from './sidecar.js';
@@ -85,8 +85,9 @@ export async function buildOptimizeBrief(devRoot, slug) {
85
85
  `.ts file as \`code\`. Hover validates it (semantic selectors, no waitForTimeout/XPath), ` +
86
86
  `soft-batches trailing assertions, and files it as a REVIEW CANDIDATE at ` +
87
87
  `.hover/cache/optimized/${slug}.spec.ts.draft — it does NOT touch your spec. If it comes ` +
88
- `back with a ✗ (a rejected check), fix that and call it again. Then tell the user the ` +
89
- `candidate path so they can diff it against __vibe_tests__/${slug}.spec.ts and promote it.`;
88
+ `back with a ✗ (a rejected check), fix that and call it again. Then show the user the ` +
89
+ `diff vs __vibe_tests__/${slug}.spec.ts; if they approve, call \`promote_optimized_spec("${slug}")\` ` +
90
+ `to apply it (it overwrites the spec + removes the draft — no manual mv). They can also promote in the VS Code cockpit.`;
90
91
  return { prompt: buildOptimizePrompt(draft, sidecar, seeds, suite, outputInstruction), original: draft };
91
92
  }
92
93
  /**
@@ -115,6 +116,61 @@ export async function saveOptimizedCandidate(devRoot, slug, llmCode) {
115
116
  await writeFile(candidatePath, code.endsWith('\n') ? code : `${code}\n`, 'utf-8');
116
117
  return { candidatePath, code };
117
118
  }
119
+ /** Find the on-disk spec whose basename is `<slug>.spec.ts` under __vibe_tests__/
120
+ * (grouped specs live in subfolders), or the flat default path if none exists. */
121
+ async function findSpecPath(devRoot, slug) {
122
+ const root = join(devRoot, '__vibe_tests__');
123
+ const want = `${slug}.spec.ts`;
124
+ const walk = async (d) => {
125
+ let entries;
126
+ try {
127
+ entries = await readdir(d, { withFileTypes: true });
128
+ }
129
+ catch {
130
+ return null;
131
+ }
132
+ for (const e of entries) {
133
+ const p = join(d, e.name);
134
+ if (e.isDirectory()) {
135
+ if (e.name === 'pages' || e.name === '.hover' || e.name === 'node_modules')
136
+ continue;
137
+ const hit = await walk(p);
138
+ if (hit)
139
+ return hit;
140
+ }
141
+ else if (e.name === want) {
142
+ return p;
143
+ }
144
+ }
145
+ return null;
146
+ };
147
+ return (await walk(root)) ?? join(root, want);
148
+ }
149
+ /**
150
+ * Promote a reviewed optimization candidate: overwrite the real spec with the
151
+ * `.hover/cache/optimized/<slug>.spec.ts.draft` and remove the draft. This is the
152
+ * one place a candidate replaces the original — done ONLY on the user's say-so
153
+ * (they reviewed the diff), never automatically as part of optimize. Re-validates
154
+ * the draft (in case it was hand-edited) before applying. Throws if no draft.
155
+ */
156
+ export async function promoteOptimizedCandidate(devRoot, slug) {
157
+ const draftPath = join(devRoot, '.hover', 'cache', 'optimized', `${slug}.spec.ts.draft`);
158
+ let draft;
159
+ try {
160
+ draft = await readFile(draftPath, 'utf-8');
161
+ }
162
+ catch {
163
+ throw new OptimizeError(`no optimization candidate for "${slug}" (looked at ${draftPath}). Run optimize first.`);
164
+ }
165
+ const check = validateSpecCode(draft);
166
+ if (!check.ok) {
167
+ throw new OptimizeError(`candidate for "${slug}" is invalid — ${check.errors.join('; ')}. Not promoting.`);
168
+ }
169
+ const specPath = await findSpecPath(devRoot, slug);
170
+ await writeFile(specPath, draft.endsWith('\n') ? draft : `${draft}\n`, 'utf-8');
171
+ await rm(draftPath, { force: true });
172
+ return { path: specPath };
173
+ }
118
174
  /**
119
175
  * Build the codegen prompt: the current spec + the observed session, plus the
120
176
  * same rules the deterministic path enforces (semantic selectors, no XPath, no
@@ -0,0 +1,14 @@
1
+ export type WikiLogKind = 'crystallize' | 'api' | 'extract' | 'heal' | 'note';
2
+ export interface WikiLogEntry {
3
+ /** ISO-8601 timestamp. */
4
+ iso: string;
5
+ kind: string;
6
+ summary: string;
7
+ }
8
+ export declare function wikiLogPath(devRoot: string): string;
9
+ /** Append one event line. Best-effort — never throws. */
10
+ export declare function appendWikiLog(devRoot: string, kind: WikiLogKind, summary: string): Promise<void>;
11
+ /** Read the log's most recent entries (oldest→newest), parsed. Total: a missing
12
+ * or malformed file yields []. */
13
+ export declare function readWikiLog(devRoot: string, limit?: number): Promise<WikiLogEntry[]>;
14
+ //# sourceMappingURL=wikiLog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wikiLog.d.ts","sourceRoot":"","sources":["../../src/specs/wikiLog.ts"],"names":[],"mappings":"AAgBA,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,KAAK,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAE9E,MAAM,WAAW,YAAY;IAC3B,0BAA0B;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAOD,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,yDAAyD;AACzD,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBtG;AAED;mCACmC;AACnC,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,SAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAYvF"}
@@ -0,0 +1,58 @@
1
+ /*
2
+ * LLM-Wiki P3 — `.hover/log.md`: an append-only, machine-parseable run history
3
+ * for the app's test wiki. One event per line:
4
+ *
5
+ * - <ISO timestamp> · <kind> · <summary>
6
+ *
7
+ * Hover writes it deterministically as it MUTATES the wiki (a spec crystallized,
8
+ * an API spec locked, Page Objects extracted) — no dependence on the agent
9
+ * remembering to log, and no prompt churn. It powers an auditable timeline (and
10
+ * a future cockpit history view). Best-effort by contract: a log failure must
11
+ * NEVER break a crystallize / extract — same rule as the memory + run ledger.
12
+ */
13
+ import { appendFile, mkdir, readFile } from 'node:fs/promises';
14
+ import { join } from 'node:path';
15
+ import { hoverDir } from './sidecar.js';
16
+ const HEADER = '# Hover log\n\n' +
17
+ "Append-only run history for this app's test wiki. One event per line: " +
18
+ '`- <ISO> · <kind> · <summary>`.\n\n';
19
+ export function wikiLogPath(devRoot) {
20
+ return join(hoverDir(devRoot), 'log.md');
21
+ }
22
+ /** Append one event line. Best-effort — never throws. */
23
+ export async function appendWikiLog(devRoot, kind, summary) {
24
+ try {
25
+ await mkdir(hoverDir(devRoot), { recursive: true });
26
+ const path = wikiLogPath(devRoot);
27
+ let existing = '';
28
+ try {
29
+ existing = await readFile(path, 'utf-8');
30
+ }
31
+ catch {
32
+ /* new file → write the header first */
33
+ }
34
+ const iso = new Date().toISOString();
35
+ const line = `- ${iso} · ${kind} · ${summary.replace(/\s+/g, ' ').trim()}\n`;
36
+ await appendFile(path, `${existing ? '' : HEADER}${line}`, 'utf-8');
37
+ }
38
+ catch {
39
+ /* best-effort: a wiki-log failure must not break the write that triggered it */
40
+ }
41
+ }
42
+ /** Read the log's most recent entries (oldest→newest), parsed. Total: a missing
43
+ * or malformed file yields []. */
44
+ export async function readWikiLog(devRoot, limit = 200) {
45
+ try {
46
+ const raw = await readFile(wikiLogPath(devRoot), 'utf-8');
47
+ const entries = [];
48
+ for (const l of raw.split('\n')) {
49
+ const m = l.match(/^-\s+(\S+)\s+·\s+(\w+)\s+·\s+(.+)$/);
50
+ if (m)
51
+ entries.push({ iso: m[1], kind: m[2], summary: m[3].trim() });
52
+ }
53
+ return entries.slice(-limit);
54
+ }
55
+ catch {
56
+ return [];
57
+ }
58
+ }
package/dist/wiki.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ export { lintWiki, parseRunStatuses } from './specs/lintWiki.js';
2
+ export type { LintResult, LintFinding, LintKind, LintSeverity } from './specs/lintWiki.js';
3
+ export { readWikiLog, wikiLogPath } from './specs/wikiLog.js';
4
+ export type { WikiLogEntry, WikiLogKind } from './specs/wikiLog.js';
5
+ export { parseBusinessMap } from './specs/businessMap.js';
6
+ export type { BusinessMapGraph, MapNode, MapEdge, MapRelation, RelationKind, CoverageStatus } from './specs/businessMap.js';
7
+ //# sourceMappingURL=wiki.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wiki.d.ts","sourceRoot":"","sources":["../src/wiki.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACjE,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAC3F,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAC9D,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,YAAY,EAAE,gBAAgB,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC"}
package/dist/wiki.js ADDED
@@ -0,0 +1,14 @@
1
+ /*
2
+ * `@hover-dev/core/wiki` — the pure-read entry over `.hover/` (the app's test
3
+ * wiki): the business-map parser + the Lint health check + the run-history log
4
+ * reader. Everything here transitively imports ONLY `node:fs` / `node:path`
5
+ * (and type-only refs) — no playwright, no ts-morph, no engine.
6
+ *
7
+ * Why a separate entry from `./engine`: a slim consumer (the VS Code cockpit)
8
+ * bundles this to render the wiki, and importing the full engine barrel would
9
+ * drag `playwright-core` + `ts-morph` into its ~220 KB .vsix. This barrel keeps
10
+ * that consumer's bundle tiny while still sharing one source of truth.
11
+ */
12
+ export { lintWiki, parseRunStatuses } from './specs/lintWiki.js';
13
+ export { readWikiLog, wikiLogPath } from './specs/wikiLog.js';
14
+ export { parseBusinessMap } from './specs/businessMap.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hover-dev/core",
3
- "version": "0.23.0",
3
+ "version": "0.25.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",
@@ -30,6 +30,10 @@
30
30
  "./engine": {
31
31
  "types": "./dist/engine.d.ts",
32
32
  "import": "./dist/engine.js"
33
+ },
34
+ "./wiki": {
35
+ "types": "./dist/wiki.d.ts",
36
+ "import": "./dist/wiki.js"
33
37
  }
34
38
  },
35
39
  "files": [