@kintsugi-ai/core 0.1.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/auth.d.ts +57 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +151 -0
- package/dist/auth.js.map +1 -0
- package/dist/baseline.d.ts +8 -0
- package/dist/baseline.d.ts.map +1 -0
- package/dist/baseline.js +114 -0
- package/dist/baseline.js.map +1 -0
- package/dist/benchmark.d.ts +65 -0
- package/dist/benchmark.d.ts.map +1 -0
- package/dist/benchmark.js +232 -0
- package/dist/benchmark.js.map +1 -0
- package/dist/capturer.d.ts +14 -0
- package/dist/capturer.d.ts.map +1 -0
- package/dist/capturer.js +92 -0
- package/dist/capturer.js.map +1 -0
- package/dist/classifier.d.ts +62 -0
- package/dist/classifier.d.ts.map +1 -0
- package/dist/classifier.js +204 -0
- package/dist/classifier.js.map +1 -0
- package/dist/comparator.d.ts +28 -0
- package/dist/comparator.d.ts.map +1 -0
- package/dist/comparator.js +184 -0
- package/dist/comparator.js.map +1 -0
- package/dist/comparator.test.d.ts +2 -0
- package/dist/comparator.test.d.ts.map +1 -0
- package/dist/comparator.test.js +73 -0
- package/dist/comparator.test.js.map +1 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +79 -0
- package/dist/config.js.map +1 -0
- package/dist/config.test.d.ts +2 -0
- package/dist/config.test.d.ts.map +1 -0
- package/dist/config.test.js +67 -0
- package/dist/config.test.js.map +1 -0
- package/dist/domdiff.d.ts +6 -0
- package/dist/domdiff.d.ts.map +1 -0
- package/dist/domdiff.js +71 -0
- package/dist/domdiff.js.map +1 -0
- package/dist/feedback.d.ts +9 -0
- package/dist/feedback.d.ts.map +1 -0
- package/dist/feedback.js +57 -0
- package/dist/feedback.js.map +1 -0
- package/dist/flowid.d.ts +3 -0
- package/dist/flowid.d.ts.map +1 -0
- package/dist/flowid.js +11 -0
- package/dist/flowid.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/recorder.d.ts +12 -0
- package/dist/recorder.d.ts.map +1 -0
- package/dist/recorder.js +86 -0
- package/dist/recorder.js.map +1 -0
- package/dist/setupPrompt.d.ts +10 -0
- package/dist/setupPrompt.d.ts.map +1 -0
- package/dist/setupPrompt.js +27 -0
- package/dist/setupPrompt.js.map +1 -0
- package/dist/types.d.ts +144 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/dist/vision.d.ts +34 -0
- package/dist/vision.d.ts.map +1 -0
- package/dist/vision.js +139 -0
- package/dist/vision.js.map +1 -0
- package/package.json +37 -0
- package/src/auth.ts +195 -0
- package/src/baseline.ts +125 -0
- package/src/benchmark.ts +307 -0
- package/src/capturer.ts +105 -0
- package/src/classifier.ts +258 -0
- package/src/comparator.test.ts +97 -0
- package/src/comparator.ts +217 -0
- package/src/config.test.ts +80 -0
- package/src/config.ts +83 -0
- package/src/domdiff.ts +62 -0
- package/src/feedback.ts +64 -0
- package/src/flowid.ts +14 -0
- package/src/index.ts +13 -0
- package/src/recorder.ts +96 -0
- package/src/setupPrompt.ts +26 -0
- package/src/types.ts +144 -0
- package/tsconfig.json +9 -0
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ClassifierContext } from './comparator.js';
|
|
2
|
+
import type { KintsugiConfig } from './types.js';
|
|
3
|
+
type Logger = {
|
|
4
|
+
info: (msg: string, data?: unknown) => void;
|
|
5
|
+
warn: (msg: string, data?: unknown) => void;
|
|
6
|
+
};
|
|
7
|
+
/** Stable per-machine fingerprint used to issue anonymous keys. */
|
|
8
|
+
export declare function computeDeviceHash(): string;
|
|
9
|
+
/** Writes (or replaces) one KEY=VALUE line in ~/.kintsugi/.env, preserving other lines. */
|
|
10
|
+
export declare function writeGlobalEnvValue(key: string, value: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Returns the hosted-API key, bootstrapping an anonymous free key (written to
|
|
13
|
+
* ~/.kintsugi/.env) when none exists. Returns undefined on network failure —
|
|
14
|
+
* callers degrade to classifier-off, never crash.
|
|
15
|
+
*/
|
|
16
|
+
export declare function ensureApiKey(options: {
|
|
17
|
+
endpoint: string;
|
|
18
|
+
tokenEnvVar: string;
|
|
19
|
+
projectDir?: string;
|
|
20
|
+
fetchImpl?: typeof fetch;
|
|
21
|
+
logger?: Logger;
|
|
22
|
+
}): Promise<string | undefined>;
|
|
23
|
+
export interface ApiAccountStatus {
|
|
24
|
+
plan: string;
|
|
25
|
+
flowsUsed: number;
|
|
26
|
+
flowLimit: number | null;
|
|
27
|
+
upgradeUrl?: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function getAccountStatus(apiKey: string, endpoint: string, fetchImpl?: typeof fetch): Promise<ApiAccountStatus | undefined>;
|
|
30
|
+
/** Stripe Payment Link URL for the $29/mo Pro upgrade, or undefined when billing is unavailable. */
|
|
31
|
+
export declare function createCheckoutUrl(apiKey: string, endpoint: string, fetchImpl?: typeof fetch): Promise<string | undefined>;
|
|
32
|
+
/**
|
|
33
|
+
* Polls account status until the plan becomes 'pro' (the Stripe webhook flips
|
|
34
|
+
* it server-side shortly after payment) or the timeout elapses. Called from
|
|
35
|
+
* the CLI and the VS Code extension after the payment link is opened.
|
|
36
|
+
*/
|
|
37
|
+
export declare function waitForPlanUpgrade(apiKey: string, endpoint: string, options?: {
|
|
38
|
+
intervalMs?: number;
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
fetchImpl?: typeof fetch;
|
|
41
|
+
shouldContinue?: () => boolean;
|
|
42
|
+
onPoll?: (status: ApiAccountStatus | undefined) => void;
|
|
43
|
+
}): Promise<ApiAccountStatus | undefined>;
|
|
44
|
+
/**
|
|
45
|
+
* Shared classifier-context builder used by the hook, `kintsugi check`, and
|
|
46
|
+
* the MCP server — one code path decides when classification is active.
|
|
47
|
+
* With bootstrapKey, an anonymous hosted key is auto-issued on first use.
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildClassifierContext(config: KintsugiConfig, options?: {
|
|
50
|
+
context?: string;
|
|
51
|
+
logger?: Logger;
|
|
52
|
+
bootstrapKey?: boolean;
|
|
53
|
+
projectDir?: string;
|
|
54
|
+
fetchImpl?: typeof fetch;
|
|
55
|
+
}): Promise<ClassifierContext | undefined>;
|
|
56
|
+
export {};
|
|
57
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,KAAK,MAAM,GAAG;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAAC,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;CAAE,CAAC;AAE3G,mEAAmE;AACnE,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,2FAA2F;AAC3F,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAmBtE;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAuB9B;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,OAAO,KAAK,GACvB,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAUvC;AAED,oGAAoG;AACpG,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,OAAO,KAAK,GACvB,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAY7B;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;IACP,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC;IAC/B,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,GAAG,SAAS,KAAK,IAAI,CAAC;CACpD,GACL,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAUvC;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,cAAc,EACtB,OAAO,GAAE;IACP,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CACrB,GACL,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAkCxC"}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hosted-API client lifecycle: anonymous key bootstrap, account status, and
|
|
3
|
+
* checkout — shared by the hook, the CLI, and the VS Code extension.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import { resolveApiToken, globalKintsugiEnvPath } from './classifier.js';
|
|
10
|
+
/** Stable per-machine fingerprint used to issue anonymous keys. */
|
|
11
|
+
export function computeDeviceHash() {
|
|
12
|
+
return createHash('sha256').update(`${os.hostname()}|${os.homedir()}`).digest('hex').slice(0, 32);
|
|
13
|
+
}
|
|
14
|
+
/** Writes (or replaces) one KEY=VALUE line in ~/.kintsugi/.env, preserving other lines. */
|
|
15
|
+
export function writeGlobalEnvValue(key, value) {
|
|
16
|
+
const envPath = globalKintsugiEnvPath();
|
|
17
|
+
fs.mkdirSync(path.dirname(envPath), { recursive: true });
|
|
18
|
+
let content = '';
|
|
19
|
+
try {
|
|
20
|
+
content = fs.readFileSync(envPath, 'utf-8');
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// no file yet
|
|
24
|
+
}
|
|
25
|
+
const line = `${key}=${value}`;
|
|
26
|
+
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
27
|
+
const re = new RegExp(`^${escapedKey}=.*$`, 'm');
|
|
28
|
+
const updated = re.test(content)
|
|
29
|
+
? content.replace(re, line)
|
|
30
|
+
: content === '' || content.endsWith('\n')
|
|
31
|
+
? content + line
|
|
32
|
+
: content + '\n' + line;
|
|
33
|
+
fs.writeFileSync(envPath, updated.endsWith('\n') ? updated : updated + '\n');
|
|
34
|
+
return envPath;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Returns the hosted-API key, bootstrapping an anonymous free key (written to
|
|
38
|
+
* ~/.kintsugi/.env) when none exists. Returns undefined on network failure —
|
|
39
|
+
* callers degrade to classifier-off, never crash.
|
|
40
|
+
*/
|
|
41
|
+
export async function ensureApiKey(options) {
|
|
42
|
+
const existing = resolveApiToken(options.tokenEnvVar, options.projectDir);
|
|
43
|
+
if (existing)
|
|
44
|
+
return existing;
|
|
45
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetchImpl(`${options.endpoint.replace(/\/$/, '')}/keys`, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: { 'Content-Type': 'application/json' },
|
|
50
|
+
body: JSON.stringify({ deviceHash: computeDeviceHash() }),
|
|
51
|
+
});
|
|
52
|
+
if (!res.ok)
|
|
53
|
+
throw new Error(`key issuance ${res.status}`);
|
|
54
|
+
const body = (await res.json());
|
|
55
|
+
if (typeof body.key !== 'string' || !body.key.startsWith('knt_')) {
|
|
56
|
+
throw new Error('key issuance returned no key');
|
|
57
|
+
}
|
|
58
|
+
writeGlobalEnvValue(options.tokenEnvVar, body.key);
|
|
59
|
+
options.logger?.info('issued anonymous kintsugi api key', { envFile: globalKintsugiEnvPath() });
|
|
60
|
+
return body.key;
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
options.logger?.warn(`could not obtain a kintsugi api key: ${err.message}`);
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export async function getAccountStatus(apiKey, endpoint, fetchImpl) {
|
|
68
|
+
try {
|
|
69
|
+
const res = await (fetchImpl ?? fetch)(`${endpoint.replace(/\/$/, '')}/keys/me`, {
|
|
70
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
71
|
+
});
|
|
72
|
+
if (!res.ok)
|
|
73
|
+
return undefined;
|
|
74
|
+
return (await res.json());
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Stripe Payment Link URL for the $29/mo Pro upgrade, or undefined when billing is unavailable. */
|
|
81
|
+
export async function createCheckoutUrl(apiKey, endpoint, fetchImpl) {
|
|
82
|
+
try {
|
|
83
|
+
const res = await (fetchImpl ?? fetch)(`${endpoint.replace(/\/$/, '')}/billing/checkout`, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
86
|
+
});
|
|
87
|
+
if (!res.ok)
|
|
88
|
+
return undefined;
|
|
89
|
+
const body = (await res.json());
|
|
90
|
+
return body.url;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Polls account status until the plan becomes 'pro' (the Stripe webhook flips
|
|
98
|
+
* it server-side shortly after payment) or the timeout elapses. Called from
|
|
99
|
+
* the CLI and the VS Code extension after the payment link is opened.
|
|
100
|
+
*/
|
|
101
|
+
export async function waitForPlanUpgrade(apiKey, endpoint, options = {}) {
|
|
102
|
+
const interval = options.intervalMs ?? 3000;
|
|
103
|
+
const deadline = Date.now() + (options.timeoutMs ?? 180_000);
|
|
104
|
+
while (Date.now() < deadline && (options.shouldContinue?.() ?? true)) {
|
|
105
|
+
const status = await getAccountStatus(apiKey, endpoint, options.fetchImpl);
|
|
106
|
+
if (status?.plan === 'pro')
|
|
107
|
+
return status;
|
|
108
|
+
options.onPoll?.(status);
|
|
109
|
+
await new Promise(resolve => setTimeout(resolve, interval));
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Shared classifier-context builder used by the hook, `kintsugi check`, and
|
|
115
|
+
* the MCP server — one code path decides when classification is active.
|
|
116
|
+
* With bootstrapKey, an anonymous hosted key is auto-issued on first use.
|
|
117
|
+
*/
|
|
118
|
+
export async function buildClassifierContext(config, options = {}) {
|
|
119
|
+
const log = options.logger;
|
|
120
|
+
if (!config.classifier?.enabled) {
|
|
121
|
+
log?.info('classifier disabled in config');
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
let apiToken = resolveApiToken(config.classifier.tokenEnvVar, options.projectDir);
|
|
125
|
+
if (!apiToken && options.bootstrapKey && config.classifier.provider === 'kintsugi') {
|
|
126
|
+
apiToken = await ensureApiKey({
|
|
127
|
+
endpoint: config.classifier.endpoint,
|
|
128
|
+
tokenEnvVar: config.classifier.tokenEnvVar,
|
|
129
|
+
projectDir: options.projectDir,
|
|
130
|
+
fetchImpl: options.fetchImpl,
|
|
131
|
+
logger: log,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
if (!apiToken) {
|
|
135
|
+
log?.warn(`no API token found (env ${config.classifier.tokenEnvVar}, <project>/.kintsugi/.env, or ~/.kintsugi/.env) — classifier disabled, large diffs will escalate without classification`);
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
log?.info('classifier active', {
|
|
139
|
+
provider: config.classifier.provider,
|
|
140
|
+
model: config.classifier.model,
|
|
141
|
+
endpoint: config.classifier.endpoint,
|
|
142
|
+
turnContext: options.context || '(none)',
|
|
143
|
+
});
|
|
144
|
+
return {
|
|
145
|
+
config: config.classifier,
|
|
146
|
+
apiToken,
|
|
147
|
+
context: options.context,
|
|
148
|
+
logger: log,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FlowMetadata, FlowRecording, CaptureResult } from './types.js';
|
|
2
|
+
export declare function getFlowDir(projectDir: string, name: string): string;
|
|
3
|
+
export declare function listFlows(projectDir: string): Promise<FlowMetadata[]>;
|
|
4
|
+
export declare function loadFlow(projectDir: string, name: string): Promise<FlowRecording>;
|
|
5
|
+
export declare function saveFlow(projectDir: string, recording: FlowRecording): Promise<void>;
|
|
6
|
+
export declare function deleteFlow(projectDir: string, name: string): Promise<void>;
|
|
7
|
+
export declare function updateBaseline(projectDir: string, name: string, capture: CaptureResult): Promise<void>;
|
|
8
|
+
//# sourceMappingURL=baseline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE7E,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEnE;AAED,wBAAsB,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAoB3E;AAED,wBAAsB,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA8DvF;AAED,wBAAsB,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAO1F;AAED,wBAAsB,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGhF;AAED,wBAAsB,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB5G"}
|
package/dist/baseline.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
export function getFlowDir(projectDir, name) {
|
|
4
|
+
return path.join(projectDir, '.kintsugi', 'flows', name);
|
|
5
|
+
}
|
|
6
|
+
export async function listFlows(projectDir) {
|
|
7
|
+
const flowsDir = path.join(projectDir, '.kintsugi', 'flows');
|
|
8
|
+
try {
|
|
9
|
+
const entries = await fs.readdir(flowsDir, { withFileTypes: true });
|
|
10
|
+
const flows = [];
|
|
11
|
+
for (const entry of entries) {
|
|
12
|
+
if (entry.isDirectory()) {
|
|
13
|
+
const metaPath = path.join(flowsDir, entry.name, 'metadata.json');
|
|
14
|
+
try {
|
|
15
|
+
const data = await fs.readFile(metaPath, 'utf-8');
|
|
16
|
+
flows.push(JSON.parse(data));
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
// Ignore invalid directories
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return flows;
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function loadFlow(projectDir, name) {
|
|
30
|
+
const flowDir = getFlowDir(projectDir, name);
|
|
31
|
+
const metaPath = path.join(flowDir, 'metadata.json');
|
|
32
|
+
const stepsPath = path.join(flowDir, 'steps.json');
|
|
33
|
+
const [metaData, stepsData] = await Promise.all([
|
|
34
|
+
fs.readFile(metaPath, 'utf-8'),
|
|
35
|
+
fs.readFile(stepsPath, 'utf-8'),
|
|
36
|
+
]);
|
|
37
|
+
const metadata = JSON.parse(metaData);
|
|
38
|
+
const steps = JSON.parse(stepsData);
|
|
39
|
+
const screenshotsDir = path.join(flowDir, 'screenshots');
|
|
40
|
+
let screenshotPaths = [];
|
|
41
|
+
try {
|
|
42
|
+
const files = await fs.readdir(screenshotsDir);
|
|
43
|
+
// Explicitly match step screenshots for each step in order
|
|
44
|
+
const stepFiles = [];
|
|
45
|
+
let allStepsFound = true;
|
|
46
|
+
for (let i = 0; i < steps.length; i++) {
|
|
47
|
+
const candidates = [`step_${i}.png`, `step-${String(i).padStart(3, '0')}.png`, `step-${i}.png`];
|
|
48
|
+
const match = candidates.find(c => files.includes(c));
|
|
49
|
+
if (match) {
|
|
50
|
+
stepFiles.push(path.join(screenshotsDir, match));
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
allStepsFound = false;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (allStepsFound && stepFiles.length > 0) {
|
|
58
|
+
screenshotPaths = stepFiles;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
screenshotPaths = files
|
|
62
|
+
.filter((f) => f.endsWith('.png'))
|
|
63
|
+
.sort()
|
|
64
|
+
.map((f) => path.join(screenshotsDir, f));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
// No screenshots directory
|
|
69
|
+
}
|
|
70
|
+
const videoPath = path.join(flowDir, 'video.webm');
|
|
71
|
+
const videoExists = await fs.stat(videoPath).then(() => true).catch(() => false);
|
|
72
|
+
// Baseline aria snapshots (semantic DOM state per step)
|
|
73
|
+
const ariaDir = path.join(flowDir, 'aria');
|
|
74
|
+
const ariaSnapshotPaths = [];
|
|
75
|
+
for (let i = 0; i < steps.length; i++) {
|
|
76
|
+
const ariaPath = path.join(ariaDir, `step_${i}.yml`);
|
|
77
|
+
const exists = await fs.stat(ariaPath).then(() => true).catch(() => false);
|
|
78
|
+
ariaSnapshotPaths.push(exists ? ariaPath : undefined);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
metadata,
|
|
82
|
+
steps,
|
|
83
|
+
screenshotPaths,
|
|
84
|
+
ariaSnapshotPaths: ariaSnapshotPaths.some(p => p !== undefined) ? ariaSnapshotPaths : undefined,
|
|
85
|
+
videoPath: videoExists ? videoPath : undefined,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export async function saveFlow(projectDir, recording) {
|
|
89
|
+
const flowDir = getFlowDir(projectDir, recording.metadata.name);
|
|
90
|
+
await fs.mkdir(flowDir, { recursive: true });
|
|
91
|
+
await fs.mkdir(path.join(flowDir, 'screenshots'), { recursive: true });
|
|
92
|
+
await fs.writeFile(path.join(flowDir, 'metadata.json'), JSON.stringify(recording.metadata, null, 2));
|
|
93
|
+
await fs.writeFile(path.join(flowDir, 'steps.json'), JSON.stringify(recording.steps, null, 2));
|
|
94
|
+
}
|
|
95
|
+
export async function deleteFlow(projectDir, name) {
|
|
96
|
+
const flowDir = getFlowDir(projectDir, name);
|
|
97
|
+
await fs.rm(flowDir, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
export async function updateBaseline(projectDir, name, capture) {
|
|
100
|
+
const flowDir = getFlowDir(projectDir, name);
|
|
101
|
+
const screenshotsDir = path.join(flowDir, 'screenshots');
|
|
102
|
+
const ariaDir = path.join(flowDir, 'aria');
|
|
103
|
+
await fs.mkdir(screenshotsDir, { recursive: true });
|
|
104
|
+
await fs.mkdir(ariaDir, { recursive: true });
|
|
105
|
+
for (let i = 0; i < capture.screenshots.length; i++) {
|
|
106
|
+
const screenshot = capture.screenshots[i];
|
|
107
|
+
await fs.writeFile(path.join(screenshotsDir, `step_${i}.png`), screenshot);
|
|
108
|
+
const aria = capture.ariaSnapshots?.[i];
|
|
109
|
+
if (aria !== undefined) {
|
|
110
|
+
await fs.writeFile(path.join(ariaDir, `step_${i}.yml`), aria, 'utf-8');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=baseline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseline.js","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export interface LatencyMetric {
|
|
2
|
+
name: string;
|
|
3
|
+
targetMs: number;
|
|
4
|
+
actualMeanMs: number;
|
|
5
|
+
p50Ms: number;
|
|
6
|
+
p95Ms: number;
|
|
7
|
+
passed: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface AccuracyMetric {
|
|
10
|
+
name: string;
|
|
11
|
+
targetPercent: number;
|
|
12
|
+
actualPercent: number;
|
|
13
|
+
description: string;
|
|
14
|
+
passed: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface BenchmarkReport {
|
|
17
|
+
timestamp: string;
|
|
18
|
+
performance: {
|
|
19
|
+
metrics: LatencyMetric[];
|
|
20
|
+
allPassed: boolean;
|
|
21
|
+
};
|
|
22
|
+
accuracy: {
|
|
23
|
+
metrics: AccuracyMetric[];
|
|
24
|
+
truePositiveRate: number;
|
|
25
|
+
falsePositiveRate: number;
|
|
26
|
+
allPassed: boolean;
|
|
27
|
+
};
|
|
28
|
+
summary: {
|
|
29
|
+
totalBenchmarks: number;
|
|
30
|
+
passedBenchmarks: number;
|
|
31
|
+
launchReady: boolean;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Creates a synthetic UI PNG buffer of the given width and height.
|
|
36
|
+
*/
|
|
37
|
+
export declare function createSyntheticUiPng(options?: {
|
|
38
|
+
width?: number;
|
|
39
|
+
height?: number;
|
|
40
|
+
removeButton?: boolean;
|
|
41
|
+
shiftLayout?: boolean;
|
|
42
|
+
addNoise?: boolean;
|
|
43
|
+
noiseAmount?: number;
|
|
44
|
+
}): Buffer;
|
|
45
|
+
/**
|
|
46
|
+
* Runs the performance latency benchmark suite.
|
|
47
|
+
*/
|
|
48
|
+
export declare function runPerformanceBenchmark(iterations?: number): Promise<{
|
|
49
|
+
metrics: LatencyMetric[];
|
|
50
|
+
allPassed: boolean;
|
|
51
|
+
}>;
|
|
52
|
+
/**
|
|
53
|
+
* Runs the accuracy benchmark suite checking True Positive Rate and False Positive Rate.
|
|
54
|
+
*/
|
|
55
|
+
export declare function runAccuracyBenchmark(sampleSize?: number): Promise<{
|
|
56
|
+
metrics: AccuracyMetric[];
|
|
57
|
+
truePositiveRate: number;
|
|
58
|
+
falsePositiveRate: number;
|
|
59
|
+
allPassed: boolean;
|
|
60
|
+
}>;
|
|
61
|
+
/**
|
|
62
|
+
* Runs the complete benchmark suite (performance and accuracy).
|
|
63
|
+
*/
|
|
64
|
+
export declare function runAllBenchmarks(): Promise<BenchmarkReport>;
|
|
65
|
+
//# sourceMappingURL=benchmark.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"benchmark.d.ts","sourceRoot":"","sources":["../src/benchmark.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE;QACX,OAAO,EAAE,aAAa,EAAE,CAAC;QACzB,SAAS,EAAE,OAAO,CAAC;KACpB,CAAC;IACF,QAAQ,EAAE;QACR,OAAO,EAAE,cAAc,EAAE,CAAC;QAC1B,gBAAgB,EAAE,MAAM,CAAC;QACzB,iBAAiB,EAAE,MAAM,CAAC;QAC1B,SAAS,EAAE,OAAO,CAAC;KACpB,CAAC;IACF,OAAO,EAAE;QACP,eAAe,EAAE,MAAM,CAAC;QACxB,gBAAgB,EAAE,MAAM,CAAC;QACzB,WAAW,EAAE,OAAO,CAAC;KACtB,CAAC;CACH;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,CAAC,EAAE;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,MAAM,CA2ET;AAUD;;GAEG;AACH,wBAAsB,uBAAuB,CAAC,UAAU,SAAK,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,aAAa,EAAE,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAAC,CA8DxH;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CAAC,UAAU,SAAK,GAAG,OAAO,CAAC;IACnE,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,gBAAgB,EAAE,MAAM,CAAC;IACzB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,OAAO,CAAC;CACpB,CAAC,CAwED;AAED;;GAEG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,eAAe,CAAC,CAmBjE"}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { PNG } from 'pngjs';
|
|
2
|
+
import { computePixelDiff, compareScreenshots } from './comparator.js';
|
|
3
|
+
import { DiffResult } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Creates a synthetic UI PNG buffer of the given width and height.
|
|
6
|
+
*/
|
|
7
|
+
export function createSyntheticUiPng(options) {
|
|
8
|
+
const width = options?.width || 1280;
|
|
9
|
+
const height = options?.height || 720;
|
|
10
|
+
const png = new PNG({ width, height });
|
|
11
|
+
// Background - light gray #F8FAFC
|
|
12
|
+
for (let y = 0; y < height; y++) {
|
|
13
|
+
for (let x = 0; x < width; x++) {
|
|
14
|
+
const idx = (y * width + x) * 4;
|
|
15
|
+
png.data[idx] = 248;
|
|
16
|
+
png.data[idx + 1] = 250;
|
|
17
|
+
png.data[idx + 2] = 252;
|
|
18
|
+
png.data[idx + 3] = 255;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// Draw Navbar (0 to 60px height) - #1E293B
|
|
22
|
+
for (let y = 0; y < 60; y++) {
|
|
23
|
+
for (let x = 0; x < width; x++) {
|
|
24
|
+
const idx = (y * width + x) * 4;
|
|
25
|
+
png.data[idx] = 30;
|
|
26
|
+
png.data[idx + 1] = 41;
|
|
27
|
+
png.data[idx + 2] = 59;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Draw Card Container
|
|
31
|
+
const cardTop = options?.shiftLayout ? 120 : 100;
|
|
32
|
+
const cardLeft = 100;
|
|
33
|
+
const cardWidth = width - 200;
|
|
34
|
+
const cardHeight = 400;
|
|
35
|
+
for (let y = cardTop; y < cardTop + cardHeight; y++) {
|
|
36
|
+
for (let x = cardLeft; x < cardLeft + cardWidth; x++) {
|
|
37
|
+
const idx = (y * width + x) * 4;
|
|
38
|
+
png.data[idx] = 255;
|
|
39
|
+
png.data[idx + 1] = 255;
|
|
40
|
+
png.data[idx + 2] = 255;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Draw Button (e.g. checkout button) inside card if not removed
|
|
44
|
+
if (!options?.removeButton) {
|
|
45
|
+
const btnTop = cardTop + 300;
|
|
46
|
+
const btnLeft = cardLeft + 50;
|
|
47
|
+
const btnWidth = 200;
|
|
48
|
+
const btnHeight = 48;
|
|
49
|
+
for (let y = btnTop; y < btnTop + btnHeight; y++) {
|
|
50
|
+
for (let x = btnLeft; x < btnLeft + btnWidth; x++) {
|
|
51
|
+
const idx = (y * width + x) * 4;
|
|
52
|
+
png.data[idx] = 37;
|
|
53
|
+
png.data[idx + 1] = 99;
|
|
54
|
+
png.data[idx + 2] = 235; // Blue #2563EB
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Add subtle subpixel/anti-aliasing noise if requested
|
|
59
|
+
if (options?.addNoise) {
|
|
60
|
+
const amount = options.noiseAmount || 0.0005; // 0.05% of pixels
|
|
61
|
+
const totalPixels = width * height;
|
|
62
|
+
const noisePixels = Math.floor(totalPixels * amount);
|
|
63
|
+
for (let i = 0; i < noisePixels; i++) {
|
|
64
|
+
const px = Math.floor(Math.random() * totalPixels);
|
|
65
|
+
const idx = px * 4;
|
|
66
|
+
// Slight luminance jitter (+/- 3 in color value)
|
|
67
|
+
png.data[idx] = Math.max(0, Math.min(255, png.data[idx] + (Math.random() > 0.5 ? 2 : -2)));
|
|
68
|
+
png.data[idx + 1] = Math.max(0, Math.min(255, png.data[idx + 1] + (Math.random() > 0.5 ? 2 : -2)));
|
|
69
|
+
png.data[idx + 2] = Math.max(0, Math.min(255, png.data[idx + 2] + (Math.random() > 0.5 ? 2 : -2)));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return PNG.sync.write(png);
|
|
73
|
+
}
|
|
74
|
+
function calculatePercentiles(values) {
|
|
75
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
76
|
+
const p50 = sorted[Math.floor(sorted.length * 0.5)] || 0;
|
|
77
|
+
const p95 = sorted[Math.floor(sorted.length * 0.95)] || sorted[sorted.length - 1] || 0;
|
|
78
|
+
const mean = values.reduce((sum, v) => sum + v, 0) / (values.length || 1);
|
|
79
|
+
return { p50, p95, mean };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Runs the performance latency benchmark suite.
|
|
83
|
+
*/
|
|
84
|
+
export async function runPerformanceBenchmark(iterations = 10) {
|
|
85
|
+
const baseImg = createSyntheticUiPng();
|
|
86
|
+
const minorImg = createSyntheticUiPng({ addNoise: true, noiseAmount: 0.001 });
|
|
87
|
+
// 1. Benchmark pixelmatch on 1280x720
|
|
88
|
+
const pixelDiffTimes = [];
|
|
89
|
+
for (let i = 0; i < iterations; i++) {
|
|
90
|
+
const t0 = performance.now();
|
|
91
|
+
await computePixelDiff(baseImg, baseImg);
|
|
92
|
+
pixelDiffTimes.push(performance.now() - t0);
|
|
93
|
+
}
|
|
94
|
+
const pixelStats = calculatePercentiles(pixelDiffTimes);
|
|
95
|
+
// 2. Fast-path comparison latency (identical)
|
|
96
|
+
const fastPathTimes = [];
|
|
97
|
+
for (let i = 0; i < iterations; i++) {
|
|
98
|
+
const t0 = performance.now();
|
|
99
|
+
await compareScreenshots(baseImg, baseImg);
|
|
100
|
+
fastPathTimes.push(performance.now() - t0);
|
|
101
|
+
}
|
|
102
|
+
const fastPathStats = calculatePercentiles(fastPathTimes);
|
|
103
|
+
// 3. Minor-band comparison latency (sub-threshold noise)
|
|
104
|
+
const tier2Times = [];
|
|
105
|
+
for (let i = 0; i < iterations; i++) {
|
|
106
|
+
const t0 = performance.now();
|
|
107
|
+
await compareScreenshots(baseImg, minorImg);
|
|
108
|
+
tier2Times.push(performance.now() - t0);
|
|
109
|
+
}
|
|
110
|
+
const tier2Stats = calculatePercentiles(tier2Times);
|
|
111
|
+
const metrics = [
|
|
112
|
+
{
|
|
113
|
+
name: 'Pixel Diff Latency (1280x720)',
|
|
114
|
+
targetMs: 50,
|
|
115
|
+
actualMeanMs: pixelStats.mean,
|
|
116
|
+
p50Ms: pixelStats.p50,
|
|
117
|
+
p95Ms: pixelStats.p95,
|
|
118
|
+
passed: pixelStats.p95 <= 75,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: 'Fast-Path Hook Latency (Identical)',
|
|
122
|
+
targetMs: 50,
|
|
123
|
+
actualMeanMs: fastPathStats.mean,
|
|
124
|
+
p50Ms: fastPathStats.p50,
|
|
125
|
+
p95Ms: fastPathStats.p95,
|
|
126
|
+
passed: fastPathStats.p95 <= 75,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'Minor-Band Hook Latency (Sub-threshold Noise)',
|
|
130
|
+
targetMs: 250,
|
|
131
|
+
actualMeanMs: tier2Stats.mean,
|
|
132
|
+
p50Ms: tier2Stats.p50,
|
|
133
|
+
p95Ms: tier2Stats.p95,
|
|
134
|
+
passed: tier2Stats.p95 <= 300,
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
return {
|
|
138
|
+
metrics,
|
|
139
|
+
allPassed: metrics.every(m => m.passed),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Runs the accuracy benchmark suite checking True Positive Rate and False Positive Rate.
|
|
144
|
+
*/
|
|
145
|
+
export async function runAccuracyBenchmark(sampleSize = 20) {
|
|
146
|
+
const baseImg = createSyntheticUiPng();
|
|
147
|
+
let truePositives = 0;
|
|
148
|
+
let falseNegatives = 0;
|
|
149
|
+
let trueNegatives = 0;
|
|
150
|
+
let falsePositives = 0;
|
|
151
|
+
// Test True Positive: Actual Regressions (missing button, layout shift)
|
|
152
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
153
|
+
const isShift = i % 2 === 0;
|
|
154
|
+
const brokenImg = createSyntheticUiPng({
|
|
155
|
+
removeButton: !isShift,
|
|
156
|
+
shiftLayout: isShift,
|
|
157
|
+
});
|
|
158
|
+
const result = await compareScreenshots(baseImg, brokenImg);
|
|
159
|
+
if (result.result === DiffResult.CHANGED || result.result === DiffResult.BROKEN) {
|
|
160
|
+
truePositives++;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
falseNegatives++;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Test False Positive: Benign changes (identical or sub-pixel noise)
|
|
167
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
168
|
+
const isNoise = i % 2 === 0;
|
|
169
|
+
const benignImg = isNoise
|
|
170
|
+
? createSyntheticUiPng({ addNoise: true, noiseAmount: 0.0003 })
|
|
171
|
+
: createSyntheticUiPng();
|
|
172
|
+
const result = await compareScreenshots(baseImg, benignImg);
|
|
173
|
+
if (result.result === DiffResult.IDENTICAL || result.result === DiffResult.MINOR) {
|
|
174
|
+
trueNegatives++;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
falsePositives++;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const tpr = (truePositives / (truePositives + falseNegatives)) * 100;
|
|
181
|
+
const fpr = (falsePositives / (falsePositives + trueNegatives)) * 100;
|
|
182
|
+
const metrics = [
|
|
183
|
+
{
|
|
184
|
+
name: 'True Positive Rate (Catches Real Regressions)',
|
|
185
|
+
targetPercent: 95.0,
|
|
186
|
+
actualPercent: tpr,
|
|
187
|
+
description: 'Percentage of genuine visual UI defects successfully caught',
|
|
188
|
+
passed: tpr >= 95.0,
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: 'False Positive Rate (Filters Benign Shifts)',
|
|
192
|
+
targetPercent: 5.0,
|
|
193
|
+
actualPercent: fpr,
|
|
194
|
+
description: 'Percentage of benign subpixel/anti-aliasing changes wrongly flagged',
|
|
195
|
+
passed: fpr <= 5.0,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: 'Identical Match Precision',
|
|
199
|
+
targetPercent: 100.0,
|
|
200
|
+
actualPercent: ((trueNegatives / sampleSize) * 100),
|
|
201
|
+
description: 'Accuracy when comparing completely unchanged views',
|
|
202
|
+
passed: ((trueNegatives / sampleSize) * 100) >= 95.0,
|
|
203
|
+
},
|
|
204
|
+
];
|
|
205
|
+
return {
|
|
206
|
+
metrics,
|
|
207
|
+
truePositiveRate: tpr,
|
|
208
|
+
falsePositiveRate: fpr,
|
|
209
|
+
allPassed: metrics.every(m => m.passed),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Runs the complete benchmark suite (performance and accuracy).
|
|
214
|
+
*/
|
|
215
|
+
export async function runAllBenchmarks() {
|
|
216
|
+
const perf = await runPerformanceBenchmark(10);
|
|
217
|
+
const acc = await runAccuracyBenchmark(20);
|
|
218
|
+
const totalBenchmarks = perf.metrics.length + acc.metrics.length;
|
|
219
|
+
const passedBenchmarks = perf.metrics.filter(m => m.passed).length +
|
|
220
|
+
acc.metrics.filter(m => m.passed).length;
|
|
221
|
+
return {
|
|
222
|
+
timestamp: new Date().toISOString(),
|
|
223
|
+
performance: perf,
|
|
224
|
+
accuracy: acc,
|
|
225
|
+
summary: {
|
|
226
|
+
totalBenchmarks,
|
|
227
|
+
passedBenchmarks,
|
|
228
|
+
launchReady: perf.allPassed && acc.allPassed,
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=benchmark.js.map
|