@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,125 @@
|
|
|
1
|
+
export declare const SESSION_RECORD_VERSION = 2;
|
|
2
|
+
/** One agent-reported finding (the ## Findings block), persisted so the
|
|
3
|
+
* ledger becomes a reusable findings log — not just a run-history list.
|
|
4
|
+
* Severity is the raw marker the agent emitted (Bug / Minor / Info / …);
|
|
5
|
+
* readers normalise it for display. */
|
|
6
|
+
export interface SessionFinding {
|
|
7
|
+
severity: string;
|
|
8
|
+
text: string;
|
|
9
|
+
/** Optional short headline (from the structured JSON findings block). */
|
|
10
|
+
title?: string;
|
|
11
|
+
/** Endpoint / method when the finding is about an API call — used to
|
|
12
|
+
* crystallize a request-based regression later. */
|
|
13
|
+
endpoint?: string;
|
|
14
|
+
method?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface SessionRecord {
|
|
17
|
+
/** Bumped to 2 when the reproducibility + outcome fields below were added.
|
|
18
|
+
* Readers must tolerate v1 records (every new field is optional). */
|
|
19
|
+
version: number;
|
|
20
|
+
/** `<sanitized-startedAt>-<rand>` — the runId, generated at run start; names
|
|
21
|
+
* the run folder `.hover/runs/<conversationId>/<id>/` and is the meta.json id. */
|
|
22
|
+
id: string;
|
|
23
|
+
/** The chat conversation this run belongs to — the folder it's grouped under,
|
|
24
|
+
* so deleting a conversation removes all its runs. */
|
|
25
|
+
conversationId?: string;
|
|
26
|
+
startedAt: string;
|
|
27
|
+
endedAt: string;
|
|
28
|
+
/** Real wall-clock of the agent run (endedAt − startedAt in ms). The bare
|
|
29
|
+
* timestamps can collapse to ~0 for an instant failure; this is explicit. */
|
|
30
|
+
durationMs?: number;
|
|
31
|
+
agent: string;
|
|
32
|
+
model?: string;
|
|
33
|
+
/** Active mode: null/absent = normal authoring, else 'api-test' / 'pentest'.
|
|
34
|
+
* A pentest record is a different artifact from a normal one. */
|
|
35
|
+
mode?: string | null;
|
|
36
|
+
prompt: string;
|
|
37
|
+
outcome: 'saved' | 'completed' | 'error' | 'aborted';
|
|
38
|
+
/** Why an error/aborted run ended — engine message, preflight failure,
|
|
39
|
+
* budget cutoff, user cancel. Makes a failed record diagnostic. */
|
|
40
|
+
errorReason?: string;
|
|
41
|
+
/** The agent's final verification prose (the Result card body), minus the
|
|
42
|
+
* Findings block. Searchable history + context, not just the prompt. */
|
|
43
|
+
summary?: string;
|
|
44
|
+
/** Parsed ## Findings — the run's actual product output. */
|
|
45
|
+
findings?: SessionFinding[];
|
|
46
|
+
/** Per-tool call counts (browser_snapshot → 12, browser_click → 8). Explains
|
|
47
|
+
* cost and feeds optimization targeting. */
|
|
48
|
+
toolCounts?: Record<string, number>;
|
|
49
|
+
/** What this run drove. envId/envName come from the editor's environment
|
|
50
|
+
* store (Local vs a remote target); url is the active dev tab. The Cloud
|
|
51
|
+
* run layer keys flakiness + scheduling off these. */
|
|
52
|
+
target?: {
|
|
53
|
+
url?: string;
|
|
54
|
+
envId?: string;
|
|
55
|
+
envName?: string;
|
|
56
|
+
};
|
|
57
|
+
/** @account labels this run logged in with — LABELS ONLY, never the
|
|
58
|
+
* username/password (same contract as spec redaction). */
|
|
59
|
+
accountLabels?: string[];
|
|
60
|
+
/** Tag of the `.hover/screenshots/<tag>` dir this run wrote to, so the UI
|
|
61
|
+
* can open the run's artifacts. (Distinct from `id` because the screenshot
|
|
62
|
+
* dir is named at MCP-launch time, before the record id exists.) */
|
|
63
|
+
screenshotTag?: string;
|
|
64
|
+
/** Chaining hook (reserved for Cloud): the prior turn's session id when this
|
|
65
|
+
* was a `--resume` follow-up, so a multi-turn conversation links as one. */
|
|
66
|
+
resumeOf?: string;
|
|
67
|
+
/** Set when the session was crystallized into a spec. */
|
|
68
|
+
specSlug?: string;
|
|
69
|
+
turns?: number;
|
|
70
|
+
costUsd?: number;
|
|
71
|
+
/** Total tokens consumed (input + output + cache) — the raw-usage counterpart
|
|
72
|
+
* to costUsd, surfaced by the widget/dashboard for users who track tokens
|
|
73
|
+
* rather than dollars. */
|
|
74
|
+
tokensUsed?: number;
|
|
75
|
+
stepCount: number;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Strip leaked function-call syntax a model sometimes emits as TEXT instead of
|
|
79
|
+
* actually invoking the tool — e.g. a final summary that ends with
|
|
80
|
+
* `call\n<invoke name="mcp__playwright__browser_wait_for">…</invoke>`. The model
|
|
81
|
+
* "writes out" the call (a known tool-calling glitch, common at end-of-turn /
|
|
82
|
+
* budget cap) and the parser renders it verbatim into the report + Done card.
|
|
83
|
+
* This keeps user-facing prose about the APP, not Hover's tooling
|
|
84
|
+
* (REPORTING_DIRECTIVE). Defensive + total: any agent can trip this.
|
|
85
|
+
*/
|
|
86
|
+
export declare function stripToolCallNoise(text: string): string;
|
|
87
|
+
/** Markdown-forced: the agent emits a plain-markdown report (REPORTING_DIRECTIVE)
|
|
88
|
+
* — ONE outcome line, `- ` bullets, and an optional `## Findings` section with
|
|
89
|
+
* `- **severity** — text` items. Parse the summary + findings from that markdown
|
|
90
|
+
* only; a stray ```json block (a non-compliant agent) is stripped, never parsed
|
|
91
|
+
* for findings and never leaked. Pure + total — no Findings block yields none. */
|
|
92
|
+
export declare function parseFindings(summary: string): {
|
|
93
|
+
summary: string;
|
|
94
|
+
findings: SessionFinding[];
|
|
95
|
+
};
|
|
96
|
+
/** Count tool_use steps by tool name for the `toolCounts` field. */
|
|
97
|
+
export declare function tallyTools(steps: {
|
|
98
|
+
kind: string;
|
|
99
|
+
tool?: string;
|
|
100
|
+
}[]): Record<string, number>;
|
|
101
|
+
/** Write one session record as `<runDir>/meta.json`. The id (runId) + the
|
|
102
|
+
* conversation are decided by the caller at run start (so screenshots + report
|
|
103
|
+
* share the folder). NEVER throws; returns the path or an error string. */
|
|
104
|
+
export declare function writeSessionRecord(devRoot: string, conversationId: string, runId: string, rec: Omit<SessionRecord, 'version' | 'id' | 'conversationId'>): Promise<{
|
|
105
|
+
path: string;
|
|
106
|
+
id: string;
|
|
107
|
+
} | {
|
|
108
|
+
error: string;
|
|
109
|
+
}>;
|
|
110
|
+
/** List every run's meta.json across all conversations: `.hover/runs/<conv>/<run>/meta.json`.
|
|
111
|
+
* Best-effort; returns [] if no runs yet. */
|
|
112
|
+
export declare function listSessionRecords(devRoot: string): Promise<{
|
|
113
|
+
path: string;
|
|
114
|
+
rec: SessionRecord;
|
|
115
|
+
}[]>;
|
|
116
|
+
/**
|
|
117
|
+
* Mark the session that produced `promptText` as crystallized: find the most
|
|
118
|
+
* recent record matching the prompt that has no `specSlug` yet, set
|
|
119
|
+
* `outcome: 'saved'` + the slug. Save-as-spec arrives as a separate WS message
|
|
120
|
+
* after the run record was already written, so this is a patch, keyed on the
|
|
121
|
+
* prompt (the `user` seed step) — tolerant by design; a miss is a no-op.
|
|
122
|
+
* NEVER throws.
|
|
123
|
+
*/
|
|
124
|
+
export declare function markSessionSaved(devRoot: string, promptText: string, specSlug: string): Promise<void>;
|
|
125
|
+
//# sourceMappingURL=sessions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../src/sessions/sessions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session ledger — one summary JSON per completed agent run, appended under
|
|
3
|
+
* `.hover/sessions/`. The local console (S3) reads these for run history +
|
|
4
|
+
* spend; Hover Cloud sync (S4) uploads them as-is.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately summary-only: full `SkillStep[]` lives in the spec sidecar for
|
|
7
|
+
* saved sessions and is dropped for unsaved ones (persisting unsaved
|
|
8
|
+
* transcripts is a privacy decision deferred to a future opt-in).
|
|
9
|
+
*
|
|
10
|
+
* Writes are best-effort: a ledger failure must never break a run or a save.
|
|
11
|
+
*/
|
|
12
|
+
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { runDir, conversationsDir } from '../specs/sidecar.js';
|
|
15
|
+
export const SESSION_RECORD_VERSION = 2;
|
|
16
|
+
/** Unescape literal "\n" / "\r\n" / "\t" sequences (e.g. an agent double-escaped
|
|
17
|
+
* its newlines) into real whitespace so markdown renders properly. */
|
|
18
|
+
function deEsc(s) {
|
|
19
|
+
return s.replace(/\\r\\n/g, '\n').replace(/\\n/g, '\n').replace(/\\t/g, ' ');
|
|
20
|
+
}
|
|
21
|
+
/** Defensive leak-guard, NOT a parse path. The agent is directed to emit a
|
|
22
|
+
* plain-markdown report (REPORTING_DIRECTIVE) — no JSON. A non-compliant agent
|
|
23
|
+
* that still wraps its report in a ```json block would otherwise leak raw JSON
|
|
24
|
+
* to the UI, so strip it: recover the `summary` field as prose when present
|
|
25
|
+
* (tolerating unescaped quotes by matching up to `","findings"`), else drop the
|
|
26
|
+
* block. Findings are NEVER extracted from JSON — they come only from the
|
|
27
|
+
* markdown `## Findings` section below. */
|
|
28
|
+
function stripJsonArtifact(summary) {
|
|
29
|
+
const block = summary.match(/```json\s*([\s\S]*?)```/i);
|
|
30
|
+
if (!block)
|
|
31
|
+
return summary;
|
|
32
|
+
const sm = block[1].match(/"summary"\s*:\s*"([\s\S]*?)"\s*,\s*"findings"/i);
|
|
33
|
+
if (sm)
|
|
34
|
+
return deEsc(sm[1].replace(/\\"/g, '"')).trim();
|
|
35
|
+
return summary.replace(block[0], '').replace(/\n{3,}/g, '\n\n').trim();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Strip leaked function-call syntax a model sometimes emits as TEXT instead of
|
|
39
|
+
* actually invoking the tool — e.g. a final summary that ends with
|
|
40
|
+
* `call\n<invoke name="mcp__playwright__browser_wait_for">…</invoke>`. The model
|
|
41
|
+
* "writes out" the call (a known tool-calling glitch, common at end-of-turn /
|
|
42
|
+
* budget cap) and the parser renders it verbatim into the report + Done card.
|
|
43
|
+
* This keeps user-facing prose about the APP, not Hover's tooling
|
|
44
|
+
* (REPORTING_DIRECTIVE). Defensive + total: any agent can trip this.
|
|
45
|
+
*/
|
|
46
|
+
export function stripToolCallNoise(text) {
|
|
47
|
+
return text
|
|
48
|
+
.replace(/<function_calls>[\s\S]*?<\/function_calls>/gi, '') // wrapper form
|
|
49
|
+
.replace(/<invoke\b[\s\S]*?<\/invoke>/gi, '') // closed call block
|
|
50
|
+
.replace(/<invoke\b[\s\S]*$/gi, '') // dangling (truncated) call
|
|
51
|
+
.replace(/<parameter\b[\s\S]*?<\/parameter>/gi, '') // stray parameter
|
|
52
|
+
.replace(/^[ \t]*call[ \t]*$/gim, '') // lone "call" lead-in line
|
|
53
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
54
|
+
.trim();
|
|
55
|
+
}
|
|
56
|
+
/** Markdown-forced: the agent emits a plain-markdown report (REPORTING_DIRECTIVE)
|
|
57
|
+
* — ONE outcome line, `- ` bullets, and an optional `## Findings` section with
|
|
58
|
+
* `- **severity** — text` items. Parse the summary + findings from that markdown
|
|
59
|
+
* only; a stray ```json block (a non-compliant agent) is stripped, never parsed
|
|
60
|
+
* for findings and never leaked. Pure + total — no Findings block yields none. */
|
|
61
|
+
export function parseFindings(summary) {
|
|
62
|
+
const cleaned = stripToolCallNoise(stripJsonArtifact(summary));
|
|
63
|
+
const lines = cleaned.split('\n');
|
|
64
|
+
let hi = -1;
|
|
65
|
+
for (let i = 0; i < lines.length; i++) {
|
|
66
|
+
const t = lines[i].trim();
|
|
67
|
+
if (/^#{1,6}\s*(findings|bugs|issues)\b/i.test(t) || /^findings\s*:/i.test(t)) {
|
|
68
|
+
hi = i;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (hi < 0)
|
|
73
|
+
return { summary: cleaned.trim(), findings: [] };
|
|
74
|
+
let j = hi + 1;
|
|
75
|
+
while (j < lines.length && lines[j].trim() === '')
|
|
76
|
+
j++;
|
|
77
|
+
const start = j;
|
|
78
|
+
while (j < lines.length && /^\s*[-*]\s+/.test(lines[j]))
|
|
79
|
+
j++;
|
|
80
|
+
const bullets = lines.slice(start, j);
|
|
81
|
+
const findings = [];
|
|
82
|
+
for (const line of bullets) {
|
|
83
|
+
const m = line.match(/^\s*[-*]\s+(?:\*\*\s*([^*]+?)\s*\*\*\s*[—–:-]?\s*)?([\s\S]+)$/);
|
|
84
|
+
if (!m)
|
|
85
|
+
continue;
|
|
86
|
+
const text = (m[2] || '').trim();
|
|
87
|
+
if (!text)
|
|
88
|
+
continue;
|
|
89
|
+
findings.push({ severity: (m[1] || 'note').trim(), text });
|
|
90
|
+
}
|
|
91
|
+
const main = lines.slice(0, hi).concat(lines.slice(j)).join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
92
|
+
return { summary: main, findings };
|
|
93
|
+
}
|
|
94
|
+
/** Count tool_use steps by tool name for the `toolCounts` field. */
|
|
95
|
+
export function tallyTools(steps) {
|
|
96
|
+
const counts = {};
|
|
97
|
+
for (const s of steps) {
|
|
98
|
+
if (s.kind !== 'step' || !s.tool)
|
|
99
|
+
continue;
|
|
100
|
+
counts[s.tool] = (counts[s.tool] ?? 0) + 1;
|
|
101
|
+
}
|
|
102
|
+
return counts;
|
|
103
|
+
}
|
|
104
|
+
/** Write one session record as `<runDir>/meta.json`. The id (runId) + the
|
|
105
|
+
* conversation are decided by the caller at run start (so screenshots + report
|
|
106
|
+
* share the folder). NEVER throws; returns the path or an error string. */
|
|
107
|
+
export async function writeSessionRecord(devRoot, conversationId, runId, rec) {
|
|
108
|
+
try {
|
|
109
|
+
const dir = runDir(devRoot, conversationId, runId);
|
|
110
|
+
await mkdir(dir, { recursive: true });
|
|
111
|
+
const record = { version: SESSION_RECORD_VERSION, id: runId, conversationId, ...rec };
|
|
112
|
+
const path = join(dir, 'meta.json');
|
|
113
|
+
await writeFile(path, JSON.stringify(record, null, 2) + '\n', 'utf-8');
|
|
114
|
+
return { path, id: runId };
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** List every run's meta.json across all conversations: `.hover/runs/<conv>/<run>/meta.json`.
|
|
121
|
+
* Best-effort; returns [] if no runs yet. */
|
|
122
|
+
export async function listSessionRecords(devRoot) {
|
|
123
|
+
const out = [];
|
|
124
|
+
const root = conversationsDir(devRoot);
|
|
125
|
+
let convs;
|
|
126
|
+
try {
|
|
127
|
+
convs = await readdir(root);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
for (const conv of convs) {
|
|
133
|
+
let runIds;
|
|
134
|
+
try {
|
|
135
|
+
runIds = await readdir(join(root, conv));
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
for (const rid of runIds) {
|
|
141
|
+
const path = join(root, conv, rid, 'meta.json');
|
|
142
|
+
try {
|
|
143
|
+
out.push({ path, rec: JSON.parse(await readFile(path, 'utf-8')) });
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
/* not a run dir / unreadable — skip */
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Mark the session that produced `promptText` as crystallized: find the most
|
|
154
|
+
* recent record matching the prompt that has no `specSlug` yet, set
|
|
155
|
+
* `outcome: 'saved'` + the slug. Save-as-spec arrives as a separate WS message
|
|
156
|
+
* after the run record was already written, so this is a patch, keyed on the
|
|
157
|
+
* prompt (the `user` seed step) — tolerant by design; a miss is a no-op.
|
|
158
|
+
* NEVER throws.
|
|
159
|
+
*/
|
|
160
|
+
export async function markSessionSaved(devRoot, promptText, specSlug) {
|
|
161
|
+
try {
|
|
162
|
+
const records = (await listSessionRecords(devRoot)).sort((a, b) => String(b.rec.startedAt).localeCompare(String(a.rec.startedAt)));
|
|
163
|
+
for (const { path, rec } of records) {
|
|
164
|
+
if (rec.specSlug || rec.prompt !== promptText)
|
|
165
|
+
continue;
|
|
166
|
+
rec.outcome = 'saved';
|
|
167
|
+
rec.specSlug = specSlug;
|
|
168
|
+
await writeFile(path, JSON.stringify(rec, null, 2) + '\n', 'utf-8');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
/* no ledger yet / unreadable — fine */
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { SkillStep } from './specStep.js';
|
|
2
|
+
/**
|
|
3
|
+
* Length of the leading login prefix among `actions` (a spec's tool steps,
|
|
4
|
+
* POST-redaction). The login flow = the steps up to AND INCLUDING the submit
|
|
5
|
+
* click that follows the LAST credential fill (e.g. navigate → type email →
|
|
6
|
+
* type password → click "Sign in"). `envVars` are the redaction env-var names.
|
|
7
|
+
*
|
|
8
|
+
* Returns 0 when there are no redacted credentials, or none are filled in the
|
|
9
|
+
* steps — so a spec with no login keeps today's inline behavior unchanged (no
|
|
10
|
+
* regression). The caller slices `actions[0..N)` as the auth prefix and
|
|
11
|
+
* `actions[N..]` as the business flow.
|
|
12
|
+
*/
|
|
13
|
+
export declare function authPrefixLength(actions: SkillStep[], envVars: readonly string[]): number;
|
|
14
|
+
/**
|
|
15
|
+
* Stage 4a — propose the playwright.config edit that registers the auth-fixture
|
|
16
|
+
* setup project. AST-based (ts-morph) so it only reprints what it touches and
|
|
17
|
+
* preserves the user's formatting. Adds:
|
|
18
|
+
*
|
|
19
|
+
* projects: [
|
|
20
|
+
* { name: 'setup', testMatch: /.*\.setup\.ts$/ },
|
|
21
|
+
* { name: 'chromium', dependencies: ['setup'] },
|
|
22
|
+
* ]
|
|
23
|
+
*
|
|
24
|
+
* Returns the edited source, or null when it can't safely edit — no config
|
|
25
|
+
* object found, or `projects` ALREADY exists (merging into a user's project
|
|
26
|
+
* matrix is risky; the caller degrades to the static paste hint instead). The
|
|
27
|
+
* edit is never applied here; the caller shows it for approval first.
|
|
28
|
+
*/
|
|
29
|
+
export declare function addSetupProjectToConfig(source: string): string | null;
|
|
30
|
+
//# sourceMappingURL=authFixture.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authFixture.d.ts","sourceRoot":"","sources":["../../src/specs/authFixture.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAiC/C;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,SAAS,EAAE,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAazF;AA0BD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAoBrE"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth-as-fixture (crystallization debt 3) — login-prefix detection.
|
|
3
|
+
*
|
|
4
|
+
* Today a recorded login is crystallized INLINE into every spec and re-run
|
|
5
|
+
* through the UI each test. The fix is to lift the login into a Playwright setup
|
|
6
|
+
* project that authenticates ONCE, save `storageState`, and have specs start
|
|
7
|
+
* already authenticated. The first, pure step is detecting which leading steps
|
|
8
|
+
* ARE the login flow — done here so it can be unit-tested in isolation, with no
|
|
9
|
+
* codegen changes (those land in later stages).
|
|
10
|
+
*
|
|
11
|
+
* Signal: `redactSteps` (writeSpec.ts) already rewrites credential values to
|
|
12
|
+
* `process.env.<envVar> ?? ''`, so the credential-bearing steps are exactly the
|
|
13
|
+
* fills whose value references one of the run's redaction env vars. The login
|
|
14
|
+
* prefix is the run of steps up to AND INCLUDING the submit click that follows
|
|
15
|
+
* the last credential fill.
|
|
16
|
+
*
|
|
17
|
+
* See docs/superpowers/specs/2026-06-24-auth-as-fixture.md.
|
|
18
|
+
*/
|
|
19
|
+
import { Project, SyntaxKind, Node } from 'ts-morph';
|
|
20
|
+
const CLICK_TOOLS = new Set(['browser_click', 'click_control']);
|
|
21
|
+
/** Bare tool name — grounded steps arrive as `mcp__hover-control__click_control`,
|
|
22
|
+
* playwright ones as bare `browser_click`. */
|
|
23
|
+
const bareTool = (t) => (t ?? '').replace(/^mcp__[a-z0-9_-]+?__/, '');
|
|
24
|
+
/** The string values a fill-type action writes, across the tool variants
|
|
25
|
+
* (browser_type / fill_control / select_control / browser_fill_form). */
|
|
26
|
+
function fillValues(step) {
|
|
27
|
+
if (step.kind !== 'step' || !step.input)
|
|
28
|
+
return [];
|
|
29
|
+
const input = step.input;
|
|
30
|
+
const out = [];
|
|
31
|
+
if (typeof input.text === 'string')
|
|
32
|
+
out.push(input.text); // browser_type
|
|
33
|
+
if (typeof input.value === 'string')
|
|
34
|
+
out.push(input.value); // fill_control / select_control
|
|
35
|
+
if (Array.isArray(input.fields)) {
|
|
36
|
+
// browser_fill_form
|
|
37
|
+
for (const f of input.fields) {
|
|
38
|
+
if (f && typeof f.value === 'string')
|
|
39
|
+
out.push(f.value);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/** True when an action fills one of the redacted credential env refs. `actions`
|
|
45
|
+
* are POST-redaction, so a credential value reads `process.env.<envVar> ?? ''`. */
|
|
46
|
+
function fillsCredential(step, envVars) {
|
|
47
|
+
if (!envVars.length)
|
|
48
|
+
return false;
|
|
49
|
+
const values = fillValues(step);
|
|
50
|
+
return values.some((v) => envVars.some((name) => v.includes(`process.env.${name}`)));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Length of the leading login prefix among `actions` (a spec's tool steps,
|
|
54
|
+
* POST-redaction). The login flow = the steps up to AND INCLUDING the submit
|
|
55
|
+
* click that follows the LAST credential fill (e.g. navigate → type email →
|
|
56
|
+
* type password → click "Sign in"). `envVars` are the redaction env-var names.
|
|
57
|
+
*
|
|
58
|
+
* Returns 0 when there are no redacted credentials, or none are filled in the
|
|
59
|
+
* steps — so a spec with no login keeps today's inline behavior unchanged (no
|
|
60
|
+
* regression). The caller slices `actions[0..N)` as the auth prefix and
|
|
61
|
+
* `actions[N..]` as the business flow.
|
|
62
|
+
*/
|
|
63
|
+
export function authPrefixLength(actions, envVars) {
|
|
64
|
+
if (!envVars.length)
|
|
65
|
+
return 0;
|
|
66
|
+
let lastCred = -1;
|
|
67
|
+
for (let i = 0; i < actions.length; i++) {
|
|
68
|
+
if (fillsCredential(actions[i], envVars))
|
|
69
|
+
lastCred = i;
|
|
70
|
+
}
|
|
71
|
+
if (lastCred < 0)
|
|
72
|
+
return 0;
|
|
73
|
+
// Extend through the submit click immediately after the last credential fill
|
|
74
|
+
// (the "Sign in" button). A non-click next step means login auto-submitted (or
|
|
75
|
+
// we've already moved into the app), so stop at the fill — don't over-capture.
|
|
76
|
+
const next = actions[lastCred + 1];
|
|
77
|
+
if (next && CLICK_TOOLS.has(bareTool(next.tool)))
|
|
78
|
+
return lastCred + 2;
|
|
79
|
+
return lastCred + 1;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Locate the Playwright config object literal — the argument of `defineConfig({…})`
|
|
83
|
+
* or a bare `export default {…}` — so the setup project can be inserted into it.
|
|
84
|
+
*/
|
|
85
|
+
function findConfigObject(sf) {
|
|
86
|
+
const def = sf.getExportAssignment((d) => !d.isExportEquals());
|
|
87
|
+
const expr = def?.getExpression();
|
|
88
|
+
if (expr) {
|
|
89
|
+
if (Node.isObjectLiteralExpression(expr))
|
|
90
|
+
return expr;
|
|
91
|
+
if (Node.isCallExpression(expr)) {
|
|
92
|
+
const arg = expr.getArguments()[0];
|
|
93
|
+
if (arg && Node.isObjectLiteralExpression(arg))
|
|
94
|
+
return arg;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Fallback: a defineConfig(...) call anywhere in the file.
|
|
98
|
+
for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
99
|
+
if (call.getExpression().getText() === 'defineConfig') {
|
|
100
|
+
const arg = call.getArguments()[0];
|
|
101
|
+
if (arg && Node.isObjectLiteralExpression(arg))
|
|
102
|
+
return arg;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Stage 4a — propose the playwright.config edit that registers the auth-fixture
|
|
109
|
+
* setup project. AST-based (ts-morph) so it only reprints what it touches and
|
|
110
|
+
* preserves the user's formatting. Adds:
|
|
111
|
+
*
|
|
112
|
+
* projects: [
|
|
113
|
+
* { name: 'setup', testMatch: /.*\.setup\.ts$/ },
|
|
114
|
+
* { name: 'chromium', dependencies: ['setup'] },
|
|
115
|
+
* ]
|
|
116
|
+
*
|
|
117
|
+
* Returns the edited source, or null when it can't safely edit — no config
|
|
118
|
+
* object found, or `projects` ALREADY exists (merging into a user's project
|
|
119
|
+
* matrix is risky; the caller degrades to the static paste hint instead). The
|
|
120
|
+
* edit is never applied here; the caller shows it for approval first.
|
|
121
|
+
*/
|
|
122
|
+
export function addSetupProjectToConfig(source) {
|
|
123
|
+
try {
|
|
124
|
+
const project = new Project({ useInMemoryFileSystem: true, compilerOptions: { allowJs: true } });
|
|
125
|
+
const sf = project.createSourceFile('__pwconfig.ts', source, { overwrite: true });
|
|
126
|
+
const obj = findConfigObject(sf);
|
|
127
|
+
if (!obj)
|
|
128
|
+
return null;
|
|
129
|
+
if (obj.getProperty('projects'))
|
|
130
|
+
return null; // user already manages projects — don't risk it
|
|
131
|
+
obj.addPropertyAssignment({
|
|
132
|
+
name: 'projects',
|
|
133
|
+
initializer: [
|
|
134
|
+
'[',
|
|
135
|
+
" { name: 'setup', testMatch: /.*\\.setup\\.ts$/ },",
|
|
136
|
+
" { name: 'chromium', dependencies: ['setup'] },",
|
|
137
|
+
' ]',
|
|
138
|
+
].join('\n'),
|
|
139
|
+
});
|
|
140
|
+
return sf.getFullText();
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type MapNodeKind = 'app' | 'area' | 'line' | 'spec';
|
|
2
|
+
export type CoverageStatus = 'covered' | 'uncovered';
|
|
3
|
+
export interface MapNode {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
kind: MapNodeKind;
|
|
7
|
+
/** Coverage of a business line (only on `line` nodes). */
|
|
8
|
+
status?: CoverageStatus;
|
|
9
|
+
/** Entry route of a business line, if given. */
|
|
10
|
+
route?: string;
|
|
11
|
+
/** Spec filename a line is covered by (on `line` and `spec` nodes). */
|
|
12
|
+
spec?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface MapEdge {
|
|
15
|
+
source: string;
|
|
16
|
+
target: string;
|
|
17
|
+
}
|
|
18
|
+
export interface BusinessMapGraph {
|
|
19
|
+
app: string;
|
|
20
|
+
nodes: MapNode[];
|
|
21
|
+
edges: MapEdge[];
|
|
22
|
+
stats: {
|
|
23
|
+
lines: number;
|
|
24
|
+
covered: number;
|
|
25
|
+
areas: number;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export declare function parseBusinessMap(md: string, fallbackApp?: string): BusinessMapGraph;
|
|
29
|
+
//# sourceMappingURL=businessMap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"businessMap.d.ts","sourceRoot":"","sources":["../../src/specs/businessMap.ts"],"names":[],"mappings":"AAeA,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,0DAA0D;IAC1D,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,gDAAgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,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;AA8BD,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,WAAW,SAAQ,GAAG,gBAAgB,CAwDlF"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Business-map parser: turn the `.hover/hover-map.md` wiki the agent maintains
|
|
3
|
+
* into a graph model the cockpit renders (areas → business lines → specs, with
|
|
4
|
+
* coverage). The map is a human-curated markdown checklist:
|
|
5
|
+
*
|
|
6
|
+
* # Business map — myapp
|
|
7
|
+
* ## Auth
|
|
8
|
+
* - [ ] Log in — /login
|
|
9
|
+
* - [x] Checkout — /checkout — checkout.spec.ts
|
|
10
|
+
*
|
|
11
|
+
* Pure + total: malformed lines are skipped, never thrown. The graph is
|
|
12
|
+
* hierarchical (app → area → line → spec); richer relationship edges
|
|
13
|
+
* (depends-on / shares-state / navigates-to) are a later format extension.
|
|
14
|
+
*/
|
|
15
|
+
function slug(s) {
|
|
16
|
+
return (s
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
19
|
+
.replace(/^-+|-+$/g, '') || 'x');
|
|
20
|
+
}
|
|
21
|
+
const SPEC_RE = /\.spec\.tsx?$/;
|
|
22
|
+
/** Split a business-line item on " — " / " – " / " - " (em/en/hyphen, spaced). */
|
|
23
|
+
function splitItem(rest) {
|
|
24
|
+
const parts = rest
|
|
25
|
+
.split(/\s+[—–-]\s+/)
|
|
26
|
+
.map((p) => p.trim())
|
|
27
|
+
.filter(Boolean);
|
|
28
|
+
const name = parts.shift() ?? rest.trim();
|
|
29
|
+
let route;
|
|
30
|
+
let spec;
|
|
31
|
+
for (const p of parts) {
|
|
32
|
+
if (SPEC_RE.test(p))
|
|
33
|
+
spec = p;
|
|
34
|
+
else if (p.startsWith('/'))
|
|
35
|
+
route = p;
|
|
36
|
+
else if (!spec && SPEC_RE.test(p))
|
|
37
|
+
spec = p;
|
|
38
|
+
}
|
|
39
|
+
return { name, route, spec };
|
|
40
|
+
}
|
|
41
|
+
export function parseBusinessMap(md, fallbackApp = 'app') {
|
|
42
|
+
const nodes = [];
|
|
43
|
+
const edges = [];
|
|
44
|
+
const seen = new Set();
|
|
45
|
+
const add = (n) => {
|
|
46
|
+
if (seen.has(n.id))
|
|
47
|
+
return;
|
|
48
|
+
seen.add(n.id);
|
|
49
|
+
nodes.push(n);
|
|
50
|
+
};
|
|
51
|
+
let app = fallbackApp;
|
|
52
|
+
// Title: `# Business map — <app>` (or any `# <title>`).
|
|
53
|
+
const title = md.match(/^#\s+(.+)$/m);
|
|
54
|
+
if (title) {
|
|
55
|
+
const t = title[1].trim();
|
|
56
|
+
const m = t.match(/business\s*map\s*[—–-]\s*(.+)$/i);
|
|
57
|
+
app = (m ? m[1] : t).trim() || fallbackApp;
|
|
58
|
+
}
|
|
59
|
+
add({ id: 'app', label: app, kind: 'app' });
|
|
60
|
+
let area = null;
|
|
61
|
+
let covered = 0;
|
|
62
|
+
let lineCount = 0;
|
|
63
|
+
let areaCount = 0;
|
|
64
|
+
for (const raw of md.split('\n')) {
|
|
65
|
+
const line = raw.trimEnd();
|
|
66
|
+
const areaM = line.match(/^##\s+(.+)$/);
|
|
67
|
+
if (areaM) {
|
|
68
|
+
const label = areaM[1].trim();
|
|
69
|
+
const id = `area:${slug(label)}`;
|
|
70
|
+
area = { id };
|
|
71
|
+
add({ id, label, kind: 'area' });
|
|
72
|
+
edges.push({ source: 'app', target: id });
|
|
73
|
+
areaCount++;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const itemM = line.match(/^\s*-\s*\[([ xX])\]\s+(.+)$/);
|
|
77
|
+
if (itemM) {
|
|
78
|
+
const status = itemM[1].toLowerCase() === 'x' ? 'covered' : 'uncovered';
|
|
79
|
+
const { name, route, spec } = splitItem(itemM[2]);
|
|
80
|
+
const parentId = area?.id ?? 'app';
|
|
81
|
+
const lineId = `line:${slug(area ? area.id.slice(5) : 'top')}/${slug(name)}`;
|
|
82
|
+
add({ id: lineId, label: name, kind: 'line', status, route, spec });
|
|
83
|
+
edges.push({ source: parentId, target: lineId });
|
|
84
|
+
lineCount++;
|
|
85
|
+
if (status === 'covered')
|
|
86
|
+
covered++;
|
|
87
|
+
if (spec) {
|
|
88
|
+
const specId = `spec:${spec}`;
|
|
89
|
+
add({ id: specId, label: spec, kind: 'spec', spec });
|
|
90
|
+
edges.push({ source: lineId, target: specId });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { app, nodes, edges, stats: { lines: lineCount, covered, areas: areaCount } };
|
|
95
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"detectSharedFlows.d.ts","sourceRoot":"","sources":["../../src/specs/detectSharedFlows.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"detectSharedFlows.d.ts","sourceRoot":"","sources":["../../src/specs/detectSharedFlows.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEtD,MAAM,WAAW,UAAU;IACzB,uDAAuD;IACvD,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB;2CACuC;IACvC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,yDAAyD;IACzD,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB;wDACoD;IACpD,WAAW,EAAE,SAAS,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B;2EACuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;sDACkD;IAClD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAgC5E;AAwDD;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,MAAM,EACf,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,UAAU,EAAE,CAAC,CAiCvB"}
|