@tangle-network/agent-runtime 0.104.0 → 0.106.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 +176 -42
- package/dist/activation-CRjVfOiy.js +203 -0
- package/dist/activation-CRjVfOiy.js.map +1 -0
- package/dist/activation-DyWB0K6E.d.ts +98 -0
- package/dist/agent.d.ts +216 -234
- package/dist/agent.js +615 -547
- package/dist/agent.js.map +1 -1
- package/dist/analyst-loop-DGdtVENg.js +517 -0
- package/dist/analyst-loop-DGdtVENg.js.map +1 -0
- package/dist/analyst-loop.d.ts +10 -43
- package/dist/analyst-loop.js +2 -11
- package/dist/backends-Dy8NujB4.js +713 -0
- package/dist/backends-Dy8NujB4.js.map +1 -0
- package/dist/candidate-execution/index.d.ts +4 -192
- package/dist/candidate-execution/index.js +5 -66
- package/dist/candidate-execution-DqMbv1rU.js +488 -0
- package/dist/candidate-execution-DqMbv1rU.js.map +1 -0
- package/dist/coder-yhVWbdWc.js +40 -0
- package/dist/coder-yhVWbdWc.js.map +1 -0
- package/dist/conversation-XucG_K3l.js +1265 -0
- package/dist/conversation-XucG_K3l.js.map +1 -0
- package/dist/conversation.d.ts +2 -804
- package/dist/conversation.js +2 -55
- package/dist/environment-provider-CfLUmKmf.js +1168 -0
- package/dist/environment-provider-CfLUmKmf.js.map +1 -0
- package/dist/environment-provider-IUGU3epE.d.ts +964 -0
- package/dist/environment-provider.d.ts +2 -78
- package/dist/environment-provider.js +2 -18
- package/dist/errors-DEAvWQPy.js +111 -0
- package/dist/errors-DEAvWQPy.js.map +1 -0
- package/dist/improvement-adapter-Cr5TBSji.d.ts +166 -0
- package/dist/improvement-cycle-C095kS_g.js +2624 -0
- package/dist/improvement-cycle-C095kS_g.js.map +1 -0
- package/dist/index-BGdxAwfO.d.ts +838 -0
- package/dist/index-C-FYUuFG.d.ts +193 -0
- package/dist/index-CbNM40xG.d.ts +140 -0
- package/dist/index-DruSKs3Z.d.ts +7224 -0
- package/dist/index-Efjb3nrQ.d.ts +671 -0
- package/dist/index.d.ts +14 -1134
- package/dist/index.js +2134 -1972
- package/dist/index.js.map +1 -1
- package/dist/intelligence.d.ts +776 -888
- package/dist/intelligence.js +1466 -1410
- package/dist/intelligence.js.map +1 -1
- package/dist/{kb-gate-CwHO0vz6.d.ts → kb-gate-C8z2juK8.d.ts} +33 -31
- package/dist/kb-gate-DpaSwXVx.js +80 -0
- package/dist/kb-gate-DpaSwXVx.js.map +1 -0
- package/dist/knowledge-Ku-mPP4f.js +433 -0
- package/dist/knowledge-Ku-mPP4f.js.map +1 -0
- package/dist/knowledge.d.ts +2 -150
- package/dist/knowledge.js +2 -40
- package/dist/local-harness-Dh8PJ0ot.d.ts +455 -0
- package/dist/loop-runner-bin-BXsFEUcf.d.ts +162 -0
- package/dist/loop-runner-bin-BwjQ1NfH.js +268 -0
- package/dist/loop-runner-bin-BwjQ1NfH.js.map +1 -0
- package/dist/loop-runner-bin.d.ts +2 -17
- package/dist/loop-runner-bin.js +2 -24
- package/dist/loops.d.ts +5 -4164
- package/dist/loops.js +8 -419
- package/dist/mcp/bin.d.ts +1 -1
- package/dist/mcp/bin.js +116 -146
- package/dist/mcp/bin.js.map +1 -1
- package/dist/mcp/index.d.ts +295 -437
- package/dist/mcp/index.js +1242 -1177
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/memory-bin.d.ts +1 -1
- package/dist/mcp/memory-bin.js +37 -16
- package/dist/mcp/memory-bin.js.map +1 -1
- package/dist/memory-server-DL6cE2Ag.js +234 -0
- package/dist/memory-server-DL6cE2Ag.js.map +1 -0
- package/dist/model-resolution-Btd9iIKV.js +98 -0
- package/dist/model-resolution-Btd9iIKV.js.map +1 -0
- package/dist/openai-tools-B68JaOCx.d.ts +24 -0
- package/dist/openai-tools-CKLy1C7M.js +44 -0
- package/dist/openai-tools-CKLy1C7M.js.map +1 -0
- package/dist/otel-export-D34IQW1C.js +843 -0
- package/dist/otel-export-D34IQW1C.js.map +1 -0
- package/dist/platform.d.ts +188 -185
- package/dist/platform.js +185 -218
- package/dist/platform.js.map +1 -1
- package/dist/prepare-B53sPV7M.js +4292 -0
- package/dist/prepare-B53sPV7M.js.map +1 -0
- package/dist/primeintellect/index.d.ts +150 -147
- package/dist/primeintellect/index.js +626 -1004
- package/dist/primeintellect/index.js.map +1 -1
- package/dist/profiles.d.ts +296 -490
- package/dist/profiles.js +722 -749
- package/dist/profiles.js.map +1 -1
- package/dist/protected-model-port-BLPH5VVv.js +722 -0
- package/dist/protected-model-port-BLPH5VVv.js.map +1 -0
- package/dist/protected-model-port-BP6Z4eau.d.ts +283 -0
- package/dist/protected-redaction--F3v1oo8.js +200 -0
- package/dist/protected-redaction--F3v1oo8.js.map +1 -0
- package/dist/redact-D-u-rrcn.js +82 -0
- package/dist/redact-D-u-rrcn.js.map +1 -0
- package/dist/redact-kbIOjfjM.d.ts +452 -0
- package/dist/researcher-CoVqNhfI.js +433 -0
- package/dist/researcher-CoVqNhfI.js.map +1 -0
- package/dist/runtime-BG0BziaP.js +5408 -0
- package/dist/runtime-BG0BziaP.js.map +1 -0
- package/dist/sandbox-events-DeI5xX8P.js +316 -0
- package/dist/sandbox-events-DeI5xX8P.js.map +1 -0
- package/dist/spawn-journal-CwPvKUTa.js +456 -0
- package/dist/spawn-journal-CwPvKUTa.js.map +1 -0
- package/dist/structural-rollout-IXUEplky.js +1445 -0
- package/dist/structural-rollout-IXUEplky.js.map +1 -0
- package/dist/substrate-B0TYNrXn.js +27 -0
- package/dist/substrate-B0TYNrXn.js.map +1 -0
- package/dist/{substrate-Dyiqmd3o.d.ts → substrate-BcnuSHXm.d.ts} +57 -54
- package/dist/supervise-BiRutHS9.js +7321 -0
- package/dist/supervise-BiRutHS9.js.map +1 -0
- package/dist/supervisor-DTKhF-RV.js +3826 -0
- package/dist/supervisor-DTKhF-RV.js.map +1 -0
- package/dist/testing.d.ts +4 -3
- package/dist/testing.js +2478 -2637
- package/dist/testing.js.map +1 -1
- package/dist/tool-server-RcWgLIsL.js +141 -0
- package/dist/tool-server-RcWgLIsL.js.map +1 -0
- package/dist/trace-propagation-CJJC7SVB.js +57 -0
- package/dist/trace-propagation-CJJC7SVB.js.map +1 -0
- package/dist/types-BaIpb0XT.d.ts +492 -0
- package/dist/types-BevOjfTY.d.ts +765 -0
- package/dist/types-C9j4qg6l.d.ts +500 -0
- package/dist/types-Cja5OTGh.d.ts +190 -0
- package/dist/util-MVgdwuIS.js +145 -0
- package/dist/util-MVgdwuIS.js.map +1 -0
- package/dist/workspace-archive-BosQiotA.js +575 -0
- package/dist/workspace-archive-BosQiotA.js.map +1 -0
- package/package.json +43 -55
- package/skills/build-with-agent-runtime/SKILL.md +16 -9
- package/skills/loop-writer/SKILL.md +1 -1
- package/dist/activation-Cm-9MMX1.d.ts +0 -69
- package/dist/analyst-loop.js.map +0 -1
- package/dist/backends-BO4Jqoe2.d.ts +0 -134
- package/dist/candidate-execution/index.js.map +0 -1
- package/dist/chunk-2KGAN2HM.js +0 -1256
- package/dist/chunk-2KGAN2HM.js.map +0 -1
- package/dist/chunk-2QJSNOGK.js +0 -48
- package/dist/chunk-2QJSNOGK.js.map +0 -1
- package/dist/chunk-2UABGGUB.js +0 -474
- package/dist/chunk-2UABGGUB.js.map +0 -1
- package/dist/chunk-2VZYRB55.js +0 -834
- package/dist/chunk-2VZYRB55.js.map +0 -1
- package/dist/chunk-2X65HYMI.js +0 -3526
- package/dist/chunk-2X65HYMI.js.map +0 -1
- package/dist/chunk-3MDZX7YU.js +0 -125
- package/dist/chunk-3MDZX7YU.js.map +0 -1
- package/dist/chunk-3PPVCS75.js +0 -199
- package/dist/chunk-3PPVCS75.js.map +0 -1
- package/dist/chunk-5HNDYWUT.js +0 -6424
- package/dist/chunk-5HNDYWUT.js.map +0 -1
- package/dist/chunk-5IBQAPVB.js +0 -29
- package/dist/chunk-5IBQAPVB.js.map +0 -1
- package/dist/chunk-5QOB7H74.js +0 -387
- package/dist/chunk-5QOB7H74.js.map +0 -1
- package/dist/chunk-ATDFUJYI.js +0 -4728
- package/dist/chunk-ATDFUJYI.js.map +0 -1
- package/dist/chunk-B7K7V22Y.js +0 -699
- package/dist/chunk-B7K7V22Y.js.map +0 -1
- package/dist/chunk-DPEUKJRO.js +0 -27
- package/dist/chunk-DPEUKJRO.js.map +0 -1
- package/dist/chunk-FD2MBMOH.js +0 -494
- package/dist/chunk-FD2MBMOH.js.map +0 -1
- package/dist/chunk-FJI3EXU3.js +0 -2402
- package/dist/chunk-FJI3EXU3.js.map +0 -1
- package/dist/chunk-HAO4CXHP.js +0 -200
- package/dist/chunk-HAO4CXHP.js.map +0 -1
- package/dist/chunk-ISPWRSEC.js +0 -408
- package/dist/chunk-ISPWRSEC.js.map +0 -1
- package/dist/chunk-KRBFHMV6.js +0 -1228
- package/dist/chunk-KRBFHMV6.js.map +0 -1
- package/dist/chunk-L5DST3QC.js +0 -351
- package/dist/chunk-L5DST3QC.js.map +0 -1
- package/dist/chunk-M22PL4GH.js +0 -171
- package/dist/chunk-M22PL4GH.js.map +0 -1
- package/dist/chunk-NURIQBJQ.js +0 -72
- package/dist/chunk-NURIQBJQ.js.map +0 -1
- package/dist/chunk-PZZKQVQV.js +0 -52
- package/dist/chunk-PZZKQVQV.js.map +0 -1
- package/dist/chunk-Q2JSAVQ3.js +0 -671
- package/dist/chunk-Q2JSAVQ3.js.map +0 -1
- package/dist/chunk-SBTWKPVR.js +0 -5576
- package/dist/chunk-SBTWKPVR.js.map +0 -1
- package/dist/chunk-SGKPNBXE.js +0 -60
- package/dist/chunk-SGKPNBXE.js.map +0 -1
- package/dist/chunk-SNSMRT6H.js +0 -680
- package/dist/chunk-SNSMRT6H.js.map +0 -1
- package/dist/chunk-STJEHM5F.js +0 -1338
- package/dist/chunk-STJEHM5F.js.map +0 -1
- package/dist/chunk-UPWGXKXB.js +0 -254
- package/dist/chunk-UPWGXKXB.js.map +0 -1
- package/dist/chunk-YEJR7IXO.js +0 -68
- package/dist/chunk-YEJR7IXO.js.map +0 -1
- package/dist/chunk-ZXICDSAK.js +0 -907
- package/dist/chunk-ZXICDSAK.js.map +0 -1
- package/dist/completion-gate-Bv47cOnn.d.ts +0 -256
- package/dist/conversation.js.map +0 -1
- package/dist/coordination-CxtzMxSM.d.ts +0 -2570
- package/dist/environment-provider.js.map +0 -1
- package/dist/improve--2A0gK1-.d.ts +0 -440
- package/dist/improvement-adapter-HAZz-7vK.d.ts +0 -203
- package/dist/knowledge.js.map +0 -1
- package/dist/local-harness-CtK9dSny.d.ts +0 -175
- package/dist/loop-runner-bin-DUOGtvOl.d.ts +0 -207
- package/dist/loop-runner-bin.js.map +0 -1
- package/dist/loops.js.map +0 -1
- package/dist/openai-tools-fnj6SRVg.d.ts +0 -46
- package/dist/protected-model-port-DzkUY3HQ.d.ts +0 -271
- package/dist/sanitize-DEbPNtyI.d.ts +0 -328
- package/dist/structural-rollout-C4mabf87.d.ts +0 -446
- package/dist/supervise-7q9F5brx.d.ts +0 -379
- package/dist/types-BCoemcXU.d.ts +0 -802
- package/dist/types-BwoZWq-i.d.ts +0 -510
- package/dist/types-Cbp72qyu.d.ts +0 -491
- package/dist/types-CmYCMbFT.d.ts +0 -203
- package/dist/types-CvNwMzQt.d.ts +0 -957
- package/dist/worktree-fanout-WsOzt8qu.d.ts +0 -999
package/dist/profiles.js
CHANGED
|
@@ -1,586 +1,572 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
} from "./
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// src/profiles/ui-auditor/slugify.ts
|
|
1
|
+
import { n as multiHarnessResearcherFanout, r as researcherProfile, t as createResearcherValidator } from "./researcher-CoVqNhfI.js";
|
|
2
|
+
import { n as UI_LENSES, t as UI_FINDING_SEVERITIES } from "./substrate-B0TYNrXn.js";
|
|
3
|
+
import { t as coderTaskToPrompt } from "./coder-yhVWbdWc.js";
|
|
4
|
+
import { promises } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
//#region src/profiles/ui-auditor/slugify.ts
|
|
7
|
+
/**
|
|
8
|
+
*
|
|
9
|
+
* Shared slug helper used by both the in-process auditor client (for
|
|
10
|
+
* screenshot filenames) and the issue writer (for issue Markdown
|
|
11
|
+
* filenames). Lowercases, normalizes, strips non-alphanumeric, trims
|
|
12
|
+
* dashes, caps at 80 chars. Throws when the result is empty so callers
|
|
13
|
+
* never silently write to a name-less filename.
|
|
14
|
+
*
|
|
15
|
+
* @experimental
|
|
16
|
+
*/
|
|
17
|
+
/** @experimental */
|
|
19
18
|
function slugify(value, fieldName) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
19
|
+
const slug = value.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
20
|
+
if (slug.length === 0) throw new Error(`ui-auditor: ${fieldName} slugified to empty string. Provide a non-empty value (got ${JSON.stringify(value)}).`);
|
|
21
|
+
return slug;
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/audit/issue-writer.ts
|
|
25
|
+
/**
|
|
26
|
+
*
|
|
27
|
+
* UI-audit issue writer — pure I/O. Takes a workspace dir + `UiFinding[]`
|
|
28
|
+
* and emits:
|
|
29
|
+
* - `<workspace>/issues/NNN--<lens>--<slug>.md` — one self-contained
|
|
30
|
+
* GitHub-issue-ready Markdown per finding, with embedded screenshot
|
|
31
|
+
* references.
|
|
32
|
+
* - `<workspace>/registry.json` — finding index for dedup and audit
|
|
33
|
+
* resume across iterations.
|
|
34
|
+
* - `<workspace>/index.md` — human-readable rollup (severity / lens /
|
|
35
|
+
* route counts plus a sorted finding list).
|
|
36
|
+
*
|
|
37
|
+
* The writer is deterministic, idempotent for `appendFindings()`, and
|
|
38
|
+
* never invokes an LLM. It assigns the next monotonic id to a finding the
|
|
39
|
+
* caller did not pre-id.
|
|
40
|
+
*
|
|
41
|
+
* @experimental
|
|
42
|
+
*/
|
|
43
|
+
const SEVERITY_ORDER = {
|
|
44
|
+
critical: 0,
|
|
45
|
+
high: 1,
|
|
46
|
+
med: 2,
|
|
47
|
+
low: 3
|
|
35
48
|
};
|
|
36
49
|
function assertValidWorkspaceDir(dir) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
if (!path.isAbsolute(dir)) {
|
|
43
|
-
throw new Error(
|
|
44
|
-
`audit-writer: workspaceDir must be an absolute path (got ${JSON.stringify(dir)})`
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
if (dir.split(path.sep).includes("..")) {
|
|
48
|
-
throw new Error(
|
|
49
|
-
`audit-writer: workspaceDir must not contain '..' segments (got ${JSON.stringify(dir)})`
|
|
50
|
-
);
|
|
51
|
-
}
|
|
50
|
+
if (typeof dir !== "string" || dir.length === 0) throw new Error(`audit-writer: workspaceDir must be a non-empty string (got ${JSON.stringify(dir)})`);
|
|
51
|
+
if (!path.isAbsolute(dir)) throw new Error(`audit-writer: workspaceDir must be an absolute path (got ${JSON.stringify(dir)})`);
|
|
52
|
+
if (dir.split(path.sep).includes("..")) throw new Error(`audit-writer: workspaceDir must not contain '..' segments (got ${JSON.stringify(dir)})`);
|
|
52
53
|
}
|
|
54
|
+
/** Create the `issues/`, `screenshots/`, and `registry.json` scaffold in a new audit workspace. @experimental */
|
|
53
55
|
async function initAuditWorkspace(workspaceDir) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
56
|
+
assertValidWorkspaceDir(workspaceDir);
|
|
57
|
+
await promises.mkdir(path.join(workspaceDir, "issues"), { recursive: true });
|
|
58
|
+
await promises.mkdir(path.join(workspaceDir, "screenshots"), { recursive: true });
|
|
59
|
+
const regPath = path.join(workspaceDir, "registry.json");
|
|
60
|
+
try {
|
|
61
|
+
await promises.access(regPath);
|
|
62
|
+
} catch {
|
|
63
|
+
await promises.writeFile(regPath, JSON.stringify({
|
|
64
|
+
schemaVersion: 1,
|
|
65
|
+
findings: [],
|
|
66
|
+
routes: {}
|
|
67
|
+
}, null, 2));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Read and validate the `registry.json` from an audit workspace. @experimental */
|
|
65
71
|
async function readAuditRegistry(workspaceDir) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (reg.schemaVersion !== 1 || !Array.isArray(reg.findings) || typeof reg.routes !== "object") {
|
|
75
|
-
throw new Error(`audit-writer: registry.json at ${regPath} has an unrecognized shape`);
|
|
76
|
-
}
|
|
77
|
-
return reg;
|
|
72
|
+
assertValidWorkspaceDir(workspaceDir);
|
|
73
|
+
const regPath = path.join(workspaceDir, "registry.json");
|
|
74
|
+
const text = await promises.readFile(regPath, "utf8");
|
|
75
|
+
const parsed = JSON.parse(text);
|
|
76
|
+
if (!parsed || typeof parsed !== "object") throw new Error(`audit-writer: registry.json at ${regPath} is not a JSON object`);
|
|
77
|
+
const reg = parsed;
|
|
78
|
+
if (reg.schemaVersion !== 1 || !Array.isArray(reg.findings) || typeof reg.routes !== "object") throw new Error(`audit-writer: registry.json at ${regPath} has an unrecognized shape`);
|
|
79
|
+
return reg;
|
|
78
80
|
}
|
|
79
81
|
async function writeAuditRegistry(workspaceDir, reg) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
const regPath = path.join(workspaceDir, "registry.json");
|
|
83
|
+
const tmpPath = `${regPath}.tmp-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
84
|
+
await promises.writeFile(tmpPath, JSON.stringify(reg, null, 2));
|
|
85
|
+
await promises.rename(tmpPath, regPath);
|
|
84
86
|
}
|
|
85
|
-
|
|
87
|
+
const workspaceLocks = /* @__PURE__ */ new Map();
|
|
86
88
|
async function withWorkspaceLock(workspaceDir, fn) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
89
|
+
const key = path.resolve(workspaceDir);
|
|
90
|
+
const prev = workspaceLocks.get(key) ?? Promise.resolve();
|
|
91
|
+
let signalDone;
|
|
92
|
+
const done = new Promise((resolve) => {
|
|
93
|
+
signalDone = resolve;
|
|
94
|
+
});
|
|
95
|
+
workspaceLocks.set(key, done);
|
|
96
|
+
try {
|
|
97
|
+
await prev;
|
|
98
|
+
return await fn();
|
|
99
|
+
} finally {
|
|
100
|
+
signalDone();
|
|
101
|
+
if (workspaceLocks.get(key) === done) workspaceLocks.delete(key);
|
|
102
|
+
}
|
|
101
103
|
}
|
|
102
104
|
function assertFindingShape(f, index) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
for (let i = 0; i < f.screenshots.length; i += 1) {
|
|
124
|
-
const s = f.screenshots[i];
|
|
125
|
-
if (!s || typeof s.path !== "string" || s.path.length === 0) {
|
|
126
|
-
throw new Error(`${where}.screenshots[${i}].path must be a non-empty string`);
|
|
127
|
-
}
|
|
128
|
-
if (s.path.split(/[/\\]/).includes("..")) {
|
|
129
|
-
throw new Error(`${where}.screenshots[${i}].path must not contain '..' segments`);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
105
|
+
const where = `audit-writer: findings[${index}]`;
|
|
106
|
+
if (!UI_LENSES.includes(f.lens)) throw new Error(`${where}: invalid lens ${JSON.stringify(f.lens)}; one of ${UI_LENSES.join("|")}`);
|
|
107
|
+
if (!UI_FINDING_SEVERITIES.includes(f.severity)) throw new Error(`${where}: invalid severity ${JSON.stringify(f.severity)}; one of ${UI_FINDING_SEVERITIES.join("|")}`);
|
|
108
|
+
for (const field of [
|
|
109
|
+
"title",
|
|
110
|
+
"route",
|
|
111
|
+
"observation",
|
|
112
|
+
"impact",
|
|
113
|
+
"suggestedFix"
|
|
114
|
+
]) {
|
|
115
|
+
const v = f[field];
|
|
116
|
+
if (typeof v !== "string" || v.trim().length === 0) throw new Error(`${where}: ${field} must be a non-empty string`);
|
|
117
|
+
}
|
|
118
|
+
if (!Array.isArray(f.screenshots) || f.screenshots.length === 0) throw new Error(`${where}: screenshots must be a non-empty array`);
|
|
119
|
+
for (let i = 0; i < f.screenshots.length; i += 1) {
|
|
120
|
+
const s = f.screenshots[i];
|
|
121
|
+
if (!s || typeof s.path !== "string" || s.path.length === 0) throw new Error(`${where}.screenshots[${i}].path must be a non-empty string`);
|
|
122
|
+
if (s.path.split(/[/\\]/).includes("..")) throw new Error(`${where}.screenshots[${i}].path must not contain '..' segments`);
|
|
123
|
+
}
|
|
132
124
|
}
|
|
133
125
|
function nextFindingId(reg) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
return max + 1;
|
|
126
|
+
let max = 0;
|
|
127
|
+
for (const f of reg.findings) if (typeof f.id === "number" && f.id > max) max = f.id;
|
|
128
|
+
return max + 1;
|
|
139
129
|
}
|
|
140
130
|
function slugifyTitle(title) {
|
|
141
|
-
|
|
131
|
+
return slugify(title, "title");
|
|
142
132
|
}
|
|
143
133
|
function renderFinding(finding) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
`<sub>Generated by agent-runtime/ui-auditor \xB7 lens=\`${finding.lens}\` \xB7 severity=\`${finding.severity}\`</sub>`,
|
|
192
|
-
""
|
|
193
|
-
].join("\n");
|
|
194
|
-
}
|
|
134
|
+
if (!finding.id) throw new Error("audit-writer: cannot render a finding without an assigned id");
|
|
135
|
+
if (finding.screenshots.length === 0) throw new Error(`audit-writer: finding #${finding.id} has no screenshots`);
|
|
136
|
+
const id = finding.id;
|
|
137
|
+
const metaLines = [`> **Issue #${String(id).padStart(3, "0")}** · **Severity:** ${finding.severity} · **Lens:** ${finding.lens}`, `> **Route:** \`${finding.route}\`${finding.url ? ` · **URL:** ${finding.url}` : ""}`];
|
|
138
|
+
if (finding.viewport) metaLines.push(`> **Viewport:** ${finding.viewport}`);
|
|
139
|
+
if (finding.selector) metaLines.push(`> **Selector:** \`${finding.selector}\``);
|
|
140
|
+
if (finding.tags && finding.tags.length > 0) metaLines.push(`> **Tags:** ${finding.tags.map((t) => `\`${t}\``).join(", ")}`);
|
|
141
|
+
if (finding.similarTo && finding.similarTo.length > 0) metaLines.push(`> **Possible duplicates:** ${finding.similarTo.map((n) => `#${String(n).padStart(3, "0")}`).join(", ")}`);
|
|
142
|
+
metaLines.push(`> _Generated: ${finding.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()}_`);
|
|
143
|
+
const reproSteps = finding.reproSteps ?? `1. Open \`${finding.route}\`${finding.url ? ` (${finding.url})` : ""}\n2. Observe the area shown in the screenshot${finding.selector ? ` (selector: \`${finding.selector}\`)` : ""}.`;
|
|
144
|
+
const evidence = finding.screenshots.map((s) => ``).join("\n\n");
|
|
145
|
+
return [
|
|
146
|
+
`# [UI] ${finding.title}`,
|
|
147
|
+
"",
|
|
148
|
+
metaLines.join("\n"),
|
|
149
|
+
"",
|
|
150
|
+
"## Observation",
|
|
151
|
+
finding.observation.trim(),
|
|
152
|
+
"",
|
|
153
|
+
"## Why it matters",
|
|
154
|
+
finding.impact.trim(),
|
|
155
|
+
"",
|
|
156
|
+
"## Steps to reproduce",
|
|
157
|
+
reproSteps,
|
|
158
|
+
"",
|
|
159
|
+
"## Suggested fix",
|
|
160
|
+
finding.suggestedFix.trim(),
|
|
161
|
+
"",
|
|
162
|
+
"## Evidence",
|
|
163
|
+
evidence,
|
|
164
|
+
"",
|
|
165
|
+
"---",
|
|
166
|
+
`<sub>Generated by agent-runtime/ui-auditor · lens=\`${finding.lens}\` · severity=\`${finding.severity}\`</sub>`,
|
|
167
|
+
""
|
|
168
|
+
].join("\n");
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Append findings to a workspace, writing one Markdown file per finding
|
|
172
|
+
* and updating registry.json. Assigns monotonically increasing ids to
|
|
173
|
+
* findings that arrived without one.
|
|
174
|
+
*
|
|
175
|
+
* Findings already carrying an id that collides with the registry are
|
|
176
|
+
* rejected — callers must either freshly mint findings (id undefined) or
|
|
177
|
+
* use a separate update path. This protects against accidental overwrite.
|
|
178
|
+
*
|
|
179
|
+
* @experimental
|
|
180
|
+
*/
|
|
195
181
|
async function appendFindings(workspaceDir, findings) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
182
|
+
assertValidWorkspaceDir(workspaceDir);
|
|
183
|
+
for (let i = 0; i < findings.length; i += 1) {
|
|
184
|
+
const f = findings[i];
|
|
185
|
+
if (!f) throw new Error(`audit-writer: findings[${i}] is undefined`);
|
|
186
|
+
assertFindingShape(f, i);
|
|
187
|
+
}
|
|
188
|
+
return withWorkspaceLock(workspaceDir, async () => {
|
|
189
|
+
await initAuditWorkspace(workspaceDir);
|
|
190
|
+
const reg = await readAuditRegistry(workspaceDir);
|
|
191
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
192
|
+
for (const f of reg.findings) if (typeof f.id === "number") usedIds.add(f.id);
|
|
193
|
+
const written = [];
|
|
194
|
+
const files = [];
|
|
195
|
+
let nextId = nextFindingId(reg);
|
|
196
|
+
for (const incoming of findings) {
|
|
197
|
+
let id = incoming.id;
|
|
198
|
+
if (id !== void 0) {
|
|
199
|
+
if (usedIds.has(id)) throw new Error(`audit-writer: incoming finding id ${id} (title=${JSON.stringify(incoming.title)}) collides with an existing registry entry`);
|
|
200
|
+
} else {
|
|
201
|
+
id = nextId;
|
|
202
|
+
while (usedIds.has(id)) id += 1;
|
|
203
|
+
nextId = id + 1;
|
|
204
|
+
}
|
|
205
|
+
usedIds.add(id);
|
|
206
|
+
const createdAt = incoming.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
207
|
+
const persisted = {
|
|
208
|
+
...incoming,
|
|
209
|
+
id,
|
|
210
|
+
createdAt
|
|
211
|
+
};
|
|
212
|
+
const slug = slugifyTitle(persisted.title);
|
|
213
|
+
const fileName = `${String(id).padStart(3, "0")}--${persisted.lens}--${slug}.md`;
|
|
214
|
+
const filePathAbs = path.join(workspaceDir, "issues", fileName);
|
|
215
|
+
const filePathRel = `issues/${fileName}`;
|
|
216
|
+
await promises.writeFile(filePathAbs, renderFinding(persisted));
|
|
217
|
+
reg.findings.push(persisted);
|
|
218
|
+
written.push(persisted);
|
|
219
|
+
files.push(filePathRel);
|
|
220
|
+
}
|
|
221
|
+
await writeAuditRegistry(workspaceDir, reg);
|
|
222
|
+
return {
|
|
223
|
+
written,
|
|
224
|
+
files
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Record screenshots taken for a route in the registry, without filing a
|
|
230
|
+
* finding. Useful when the auditor wants to remember which captures
|
|
231
|
+
* exist for resume / dedup purposes.
|
|
232
|
+
*
|
|
233
|
+
* @experimental
|
|
234
|
+
*/
|
|
241
235
|
async function registerCaptures(workspaceDir, options) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
236
|
+
assertValidWorkspaceDir(workspaceDir);
|
|
237
|
+
if (typeof options.route !== "string" || options.route.trim().length === 0) throw new Error("audit-writer: registerCaptures: route must be a non-empty string");
|
|
238
|
+
return withWorkspaceLock(workspaceDir, async () => {
|
|
239
|
+
await initAuditWorkspace(workspaceDir);
|
|
240
|
+
const reg = await readAuditRegistry(workspaceDir);
|
|
241
|
+
const slot = reg.routes[options.route] ?? { captures: [] };
|
|
242
|
+
if (options.url) slot.url = options.url;
|
|
243
|
+
slot.captures = [...slot.captures, ...options.captures];
|
|
244
|
+
reg.routes[options.route] = slot;
|
|
245
|
+
await writeAuditRegistry(workspaceDir, reg);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
/** Compute finding counts by severity, lens, and route from an `AuditRegistry`. @experimental */
|
|
256
249
|
function summarizeRegistry(reg) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
250
|
+
const bySeverity = {
|
|
251
|
+
critical: 0,
|
|
252
|
+
high: 0,
|
|
253
|
+
med: 0,
|
|
254
|
+
low: 0
|
|
255
|
+
};
|
|
256
|
+
const byLens = {};
|
|
257
|
+
const byRoute = {};
|
|
258
|
+
for (const f of reg.findings) {
|
|
259
|
+
bySeverity[f.severity] += 1;
|
|
260
|
+
byLens[f.lens] = (byLens[f.lens] ?? 0) + 1;
|
|
261
|
+
byRoute[f.route] = (byRoute[f.route] ?? 0) + 1;
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
total: reg.findings.length,
|
|
265
|
+
bySeverity,
|
|
266
|
+
byLens,
|
|
267
|
+
byRoute
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Regenerate `<workspace>/index.md` from registry.json.
|
|
272
|
+
*
|
|
273
|
+
* @experimental
|
|
274
|
+
*/
|
|
272
275
|
async function writeAuditIndex(workspaceDir) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
// src/profiles/ui-auditor/prompt.ts
|
|
323
|
-
var ENVELOPE_BEGIN = "<<UI_AUDIT_TASK>>";
|
|
324
|
-
var ENVELOPE_END = "<<UI_AUDIT_TASK_END>>";
|
|
276
|
+
assertValidWorkspaceDir(workspaceDir);
|
|
277
|
+
const reg = await readAuditRegistry(workspaceDir);
|
|
278
|
+
const summary = summarizeRegistry(reg);
|
|
279
|
+
const sorted = [...reg.findings].sort((a, b) => {
|
|
280
|
+
const sa = SEVERITY_ORDER[a.severity];
|
|
281
|
+
const sb = SEVERITY_ORDER[b.severity];
|
|
282
|
+
if (sa !== sb) return sa - sb;
|
|
283
|
+
return (a.id ?? 0) - (b.id ?? 0);
|
|
284
|
+
});
|
|
285
|
+
const lines = [];
|
|
286
|
+
lines.push("# UI audit index", "");
|
|
287
|
+
lines.push(`_Generated: ${(/* @__PURE__ */ new Date()).toISOString()}_`, "");
|
|
288
|
+
lines.push(`**Total findings:** ${summary.total}`, "");
|
|
289
|
+
lines.push("## By severity");
|
|
290
|
+
for (const s of [
|
|
291
|
+
"critical",
|
|
292
|
+
"high",
|
|
293
|
+
"med",
|
|
294
|
+
"low"
|
|
295
|
+
]) {
|
|
296
|
+
const n = summary.bySeverity[s] ?? 0;
|
|
297
|
+
if (n > 0) lines.push(`- **${s}** — ${n}`);
|
|
298
|
+
}
|
|
299
|
+
lines.push("", "## By lens");
|
|
300
|
+
const lensEntries = Object.entries(summary.byLens).map(([k, v]) => [k, v ?? 0]);
|
|
301
|
+
for (const [lens, n] of lensEntries.sort((a, b) => b[1] - a[1])) lines.push(`- **${lens}** — ${n}`);
|
|
302
|
+
lines.push("", "## By route");
|
|
303
|
+
const routeEntries = Object.entries(summary.byRoute).map(([k, v]) => [k, v ?? 0]);
|
|
304
|
+
for (const [route, n] of routeEntries.sort((a, b) => b[1] - a[1])) lines.push(`- \`${route}\` — ${n}`);
|
|
305
|
+
lines.push("", "## Findings", "");
|
|
306
|
+
for (const f of sorted) {
|
|
307
|
+
if (typeof f.id !== "number") continue;
|
|
308
|
+
const num = String(f.id).padStart(3, "0");
|
|
309
|
+
const slug = slugifyTitle(f.title);
|
|
310
|
+
const file = `issues/${num}--${f.lens}--${slug}.md`;
|
|
311
|
+
lines.push(`- [#${num} \`${f.severity}\` \`${f.lens}\` \`${f.route}\` — ${f.title}](${file})`);
|
|
312
|
+
}
|
|
313
|
+
lines.push("");
|
|
314
|
+
const out = path.join(workspaceDir, "index.md");
|
|
315
|
+
const body = lines.join("\n");
|
|
316
|
+
await promises.writeFile(out, body);
|
|
317
|
+
return out;
|
|
318
|
+
}
|
|
319
|
+
//#endregion
|
|
320
|
+
//#region src/profiles/ui-auditor/prompt.ts
|
|
321
|
+
const ENVELOPE_BEGIN = "<<UI_AUDIT_TASK>>";
|
|
322
|
+
const ENVELOPE_END = "<<UI_AUDIT_TASK_END>>";
|
|
323
|
+
/** Wrap a `UiAuditTask` in a machine-readable envelope so iterations are self-describing. @experimental */
|
|
325
324
|
function encodeAuditTaskEnvelope(task) {
|
|
326
|
-
|
|
327
|
-
}
|
|
325
|
+
return `${ENVELOPE_BEGIN}${JSON.stringify(task)}${ENVELOPE_END}`;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Parse a task envelope back out of a prompt string. Returns undefined if
|
|
329
|
+
* the prompt does not contain a complete envelope OR if the payload is
|
|
330
|
+
* not valid JSON.
|
|
331
|
+
*
|
|
332
|
+
* @experimental
|
|
333
|
+
*/
|
|
328
334
|
function decodeAuditTaskEnvelope(prompt) {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
335
|
+
const start = prompt.indexOf(ENVELOPE_BEGIN);
|
|
336
|
+
if (start === -1) return void 0;
|
|
337
|
+
const payloadStart = start + 17;
|
|
338
|
+
const end = prompt.indexOf(ENVELOPE_END, payloadStart);
|
|
339
|
+
if (end === -1) return void 0;
|
|
340
|
+
const payload = prompt.slice(payloadStart, end);
|
|
341
|
+
try {
|
|
342
|
+
const parsed = JSON.parse(payload);
|
|
343
|
+
if (!parsed || typeof parsed !== "object") return void 0;
|
|
344
|
+
const t = parsed;
|
|
345
|
+
if (typeof t.lens !== "string" || !Array.isArray(t.captures)) return void 0;
|
|
346
|
+
return t;
|
|
347
|
+
} catch {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/** Produce the user message for one audit iteration: lens, captures to take, and the task envelope. @experimental */
|
|
345
352
|
function formatAuditorPrompt(task) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// src/profiles/ui-auditor/in-process-client.ts
|
|
353
|
+
const lines = [];
|
|
354
|
+
lines.push(`# UI audit iteration — lens: ${task.lens}`);
|
|
355
|
+
lines.push("");
|
|
356
|
+
if (task.productContext && task.productContext.trim().length > 0) {
|
|
357
|
+
lines.push("## Product context");
|
|
358
|
+
lines.push(task.productContext.trim());
|
|
359
|
+
lines.push("");
|
|
360
|
+
}
|
|
361
|
+
lines.push("## Captures to take");
|
|
362
|
+
task.captures.forEach((cap, i) => {
|
|
363
|
+
const detail = [
|
|
364
|
+
`viewport=${cap.viewport ? `${cap.viewport.width}x${cap.viewport.height}` : "1280x800 (default)"}`,
|
|
365
|
+
cap.fullPage ? "fullPage=true" : null,
|
|
366
|
+
cap.elementSelector ? `selector=\`${cap.elementSelector}\`` : null,
|
|
367
|
+
cap.waitFor ? `waitFor=\`${cap.waitFor}\`` : null,
|
|
368
|
+
cap.waitMs !== void 0 ? `waitMs=${cap.waitMs}` : null,
|
|
369
|
+
cap.label ? `label=${cap.label}` : null
|
|
370
|
+
].filter((s) => s !== null).join(" · ");
|
|
371
|
+
lines.push(`${i + 1}. route=\`${cap.route}\` url=${cap.url} ${detail ? `(${detail})` : ""}`);
|
|
372
|
+
});
|
|
373
|
+
lines.push("");
|
|
374
|
+
if (task.knownFindingIds && task.knownFindingIds.length > 0) {
|
|
375
|
+
lines.push("## Known findings (link via similarTo, do not refile)");
|
|
376
|
+
lines.push(task.knownFindingIds.map((n) => `#${String(n).padStart(3, "0")}`).join(", "));
|
|
377
|
+
lines.push("");
|
|
378
|
+
}
|
|
379
|
+
lines.push("## Output format");
|
|
380
|
+
lines.push("Emit a single JSON object with the shape `{ findings: UiFinding[], notes?: string }` where every finding has the fields enumerated in your system prompt. The screenshots field on each finding must reference the captures above by path. Do not emit findings outside the lens.");
|
|
381
|
+
return lines.join("\n");
|
|
382
|
+
}
|
|
383
|
+
//#endregion
|
|
384
|
+
//#region src/profiles/ui-auditor/in-process-client.ts
|
|
381
385
|
function asSandboxEvent(type, data) {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
+
return {
|
|
387
|
+
type,
|
|
388
|
+
data
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
const DEFAULT_VIEWPORT = {
|
|
392
|
+
width: 1280,
|
|
393
|
+
height: 800
|
|
394
|
+
};
|
|
395
|
+
const NAV_TIMEOUT_MS = 3e4;
|
|
386
396
|
async function defaultLaunch() {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
"ui-auditor: playwright is not installed. Install `playwright` (and run `playwright install chromium`) or pass a custom `launchBrowser` to createInProcessUiAuditClient."
|
|
391
|
-
);
|
|
392
|
-
}
|
|
393
|
-
return mod.chromium.launch({ headless: true });
|
|
397
|
+
const mod = await import("playwright");
|
|
398
|
+
if (!mod?.chromium || typeof mod.chromium.launch !== "function") throw new Error("ui-auditor: playwright is not installed. Install `playwright` (and run `playwright install chromium`) or pass a custom `launchBrowser` to createInProcessUiAuditClient.");
|
|
399
|
+
return mod.chromium.launch({ headless: true });
|
|
394
400
|
}
|
|
395
401
|
function nowStamp() {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
402
|
+
const d = /* @__PURE__ */ new Date();
|
|
403
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
404
|
+
return `${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}-${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}-${String(d.getUTCMilliseconds()).padStart(3, "0")}`;
|
|
399
405
|
}
|
|
400
406
|
function viewportOf(req) {
|
|
401
|
-
|
|
407
|
+
return req.viewport ?? DEFAULT_VIEWPORT;
|
|
402
408
|
}
|
|
403
409
|
function captureFilename(req) {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
410
|
+
const vp = viewportOf(req);
|
|
411
|
+
const labelPart = req.label ? `--${slugify(req.label, "label")}` : "";
|
|
412
|
+
return `${slugify(req.route, "route")}--${vp.width}x${vp.height}${labelPart}--${nowStamp()}.png`;
|
|
407
413
|
}
|
|
408
414
|
function assertHttpUrl(url) {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
throw new Error(
|
|
417
|
-
`ui-auditor: capture url must use http or https (got ${parsed.protocol} in ${JSON.stringify(url)})`
|
|
418
|
-
);
|
|
419
|
-
}
|
|
415
|
+
let parsed;
|
|
416
|
+
try {
|
|
417
|
+
parsed = new URL(url);
|
|
418
|
+
} catch {
|
|
419
|
+
throw new Error(`ui-auditor: capture url is not parseable (got ${JSON.stringify(url)})`);
|
|
420
|
+
}
|
|
421
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error(`ui-auditor: capture url must use http or https (got ${parsed.protocol} in ${JSON.stringify(url)})`);
|
|
420
422
|
}
|
|
421
423
|
async function captureOne(page, req, outAbsPath, signal, navPolicy) {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
424
|
+
signal.throwIfAborted();
|
|
425
|
+
assertHttpUrl(req.url);
|
|
426
|
+
await page.setViewportSize(viewportOf(req));
|
|
427
|
+
const waitUntil = navPolicy === "spa" ? "domcontentloaded" : "networkidle";
|
|
428
|
+
await page.goto(req.url, {
|
|
429
|
+
waitUntil,
|
|
430
|
+
timeout: NAV_TIMEOUT_MS
|
|
431
|
+
});
|
|
432
|
+
if (req.waitFor) await page.waitForSelector(req.waitFor, { timeout: 15e3 });
|
|
433
|
+
const extra = req.waitMs ?? 500;
|
|
434
|
+
if (extra > 0) await page.waitForTimeout(extra);
|
|
435
|
+
signal.throwIfAborted();
|
|
436
|
+
if (req.elementSelector) await page.locator(req.elementSelector).first().screenshot({ path: outAbsPath });
|
|
437
|
+
else await page.screenshot({
|
|
438
|
+
path: outAbsPath,
|
|
439
|
+
fullPage: req.fullPage === true
|
|
440
|
+
});
|
|
438
441
|
}
|
|
439
442
|
function makeSandboxId() {
|
|
440
|
-
|
|
441
|
-
|
|
443
|
+
const rand = () => Math.random().toString(16).slice(2, 10);
|
|
444
|
+
return `ui-audit-${rand()}${rand()}`;
|
|
442
445
|
}
|
|
446
|
+
/** Create a `SandboxClient` that drives a local Playwright browser for in-process UI audits. @experimental */
|
|
443
447
|
function createInProcessUiAuditClient(options) {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
const id = box.id;
|
|
556
|
-
return { kind: "sibling", sandboxId: typeof id === "string" ? id : void 0 };
|
|
557
|
-
},
|
|
558
|
-
async close() {
|
|
559
|
-
closed = true;
|
|
560
|
-
const pending = browserPromise;
|
|
561
|
-
browserPromise = void 0;
|
|
562
|
-
if (pending) {
|
|
563
|
-
const browser = await pending;
|
|
564
|
-
await browser.close();
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
};
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// src/profiles/ui-auditor/lens-prompts.ts
|
|
571
|
-
var SHARED_AUDITOR_RULES = `
|
|
572
|
-
You are auditing a UI for a specific class of problems. Stay strictly in your assigned lens \u2014 do not file issues that belong to another lens (a separate iteration will catch those).
|
|
448
|
+
const launch = options.launchBrowser ?? defaultLaunch;
|
|
449
|
+
const navPolicy = options.navPolicy ?? "strict";
|
|
450
|
+
let browserPromise;
|
|
451
|
+
let closed = false;
|
|
452
|
+
async function getBrowser() {
|
|
453
|
+
if (closed) throw new Error("ui-auditor: client is closed; create a new client to run another iteration");
|
|
454
|
+
if (!browserPromise) browserPromise = launch();
|
|
455
|
+
return browserPromise;
|
|
456
|
+
}
|
|
457
|
+
async function* runIteration(promptText, signal) {
|
|
458
|
+
const task = decodeAuditTaskEnvelope(promptText);
|
|
459
|
+
if (!task) throw new Error("ui-auditor: prompt is missing a UI_AUDIT_TASK envelope. Use uiAuditorProfile().taskToPrompt to format prompts, or pass an envelope-prefixed prompt manually.");
|
|
460
|
+
if (task.captures.length === 0) throw new Error("ui-auditor: task has zero captures; nothing to audit.");
|
|
461
|
+
yield asSandboxEvent("audit.lens", { lens: task.lens });
|
|
462
|
+
const context = await (await getBrowser()).newContext({ viewport: DEFAULT_VIEWPORT });
|
|
463
|
+
let primaryError;
|
|
464
|
+
let closeError;
|
|
465
|
+
try {
|
|
466
|
+
const page = await context.newPage();
|
|
467
|
+
const captures = [];
|
|
468
|
+
const fs = await import("node:fs/promises");
|
|
469
|
+
const path = await import("node:path");
|
|
470
|
+
const shotsDir = path.join(options.workspaceDir, "screenshots");
|
|
471
|
+
await fs.mkdir(shotsDir, { recursive: true });
|
|
472
|
+
for (const req of task.captures) {
|
|
473
|
+
signal.throwIfAborted();
|
|
474
|
+
const filename = captureFilename(req);
|
|
475
|
+
const absPath = path.join(shotsDir, filename);
|
|
476
|
+
const relPath = `screenshots/${filename}`;
|
|
477
|
+
await captureOne(page, req, absPath, signal, navPolicy);
|
|
478
|
+
const vp = viewportOf(req);
|
|
479
|
+
const cap = {
|
|
480
|
+
path: relPath,
|
|
481
|
+
viewport: `${vp.width}x${vp.height}`,
|
|
482
|
+
fullPage: req.fullPage === true,
|
|
483
|
+
route: req.route,
|
|
484
|
+
url: req.url,
|
|
485
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
486
|
+
};
|
|
487
|
+
if (req.elementSelector) cap.elementSelector = req.elementSelector;
|
|
488
|
+
if (req.label) cap.label = req.label;
|
|
489
|
+
captures.push(cap);
|
|
490
|
+
yield asSandboxEvent("audit.capture", cap);
|
|
491
|
+
}
|
|
492
|
+
const judgeOut = await options.judge({
|
|
493
|
+
lens: task.lens,
|
|
494
|
+
captures,
|
|
495
|
+
productContext: task.productContext,
|
|
496
|
+
knownFindingIds: task.knownFindingIds,
|
|
497
|
+
promptText,
|
|
498
|
+
signal
|
|
499
|
+
});
|
|
500
|
+
for (const finding of judgeOut.findings) yield asSandboxEvent("audit.finding", finding);
|
|
501
|
+
if (judgeOut.notes && judgeOut.notes.trim().length > 0) yield asSandboxEvent("audit.notes", { notes: judgeOut.notes });
|
|
502
|
+
const usage = judgeOut.tokenUsage ?? {
|
|
503
|
+
input: 0,
|
|
504
|
+
output: 0
|
|
505
|
+
};
|
|
506
|
+
yield asSandboxEvent("done", {
|
|
507
|
+
tokenUsage: {
|
|
508
|
+
inputTokens: usage.input,
|
|
509
|
+
outputTokens: usage.output
|
|
510
|
+
},
|
|
511
|
+
totalCostUsd: judgeOut.costUsd ?? 0
|
|
512
|
+
});
|
|
513
|
+
} catch (err) {
|
|
514
|
+
primaryError = err;
|
|
515
|
+
} finally {
|
|
516
|
+
try {
|
|
517
|
+
await context.close();
|
|
518
|
+
} catch (err) {
|
|
519
|
+
closeError = err;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (primaryError !== void 0 && closeError !== void 0) throw new AggregateError([primaryError, closeError], "ui-auditor: iteration failed AND context.close() failed; both errors attached.");
|
|
523
|
+
if (primaryError !== void 0) throw primaryError;
|
|
524
|
+
if (closeError !== void 0) throw closeError;
|
|
525
|
+
}
|
|
526
|
+
function makeSyntheticSandbox() {
|
|
527
|
+
return {
|
|
528
|
+
id: makeSandboxId(),
|
|
529
|
+
streamPrompt(message, opts) {
|
|
530
|
+
return runIteration(message, opts?.signal ?? new AbortController().signal);
|
|
531
|
+
},
|
|
532
|
+
async delete() {}
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
return {
|
|
536
|
+
async create(_options) {
|
|
537
|
+
return makeSyntheticSandbox();
|
|
538
|
+
},
|
|
539
|
+
describePlacement(box) {
|
|
540
|
+
const id = box.id;
|
|
541
|
+
return {
|
|
542
|
+
kind: "sibling",
|
|
543
|
+
sandboxId: typeof id === "string" ? id : void 0
|
|
544
|
+
};
|
|
545
|
+
},
|
|
546
|
+
async close() {
|
|
547
|
+
closed = true;
|
|
548
|
+
const pending = browserPromise;
|
|
549
|
+
browserPromise = void 0;
|
|
550
|
+
if (pending) await (await pending).close();
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
//#endregion
|
|
555
|
+
//#region src/profiles/ui-auditor/lens-prompts.ts
|
|
556
|
+
/** Cross-lens rules injected into every UI audit iteration: finding quality standards and scope limits. @experimental */
|
|
557
|
+
const SHARED_AUDITOR_RULES = `
|
|
558
|
+
You are auditing a UI for a specific class of problems. Stay strictly in your assigned lens — do not file issues that belong to another lens (a separate iteration will catch those).
|
|
573
559
|
|
|
574
560
|
A finding is only valid if a thoughtful product designer would agree the screenshot shows something that should change. Avoid:
|
|
575
561
|
- Personal taste ("I'd prefer brand blue").
|
|
576
562
|
- Hallucinated text or controls you cannot actually see in the screenshot.
|
|
577
563
|
- Suggestions that depend on requirements you don't have access to.
|
|
578
|
-
- Pile-on findings about the same root cause
|
|
564
|
+
- Pile-on findings about the same root cause — file ONE finding and use \`similarTo\` to link the rest.
|
|
579
565
|
|
|
580
566
|
Required for every finding:
|
|
581
567
|
- title: concrete, names the offending element AND what's wrong (NOT "improve UX").
|
|
582
568
|
- severity: critical=blocks a core task or accessibility blocker; high=noticeable friction; med=visible polish issue; low=nitpick.
|
|
583
|
-
- observation: 1
|
|
569
|
+
- observation: 1–3 sentences describing exactly what you see that is wrong.
|
|
584
570
|
- impact: who is affected and how (concrete).
|
|
585
571
|
- suggestedFix: a specific change a developer could apply without asking you back.
|
|
586
572
|
- screenshots: refer to the captures attached to this iteration by path.
|
|
@@ -588,286 +574,273 @@ Required for every finding:
|
|
|
588
574
|
|
|
589
575
|
Most findings are med or low. Reserve high/critical for genuine blockers.
|
|
590
576
|
`.trim();
|
|
591
|
-
|
|
592
|
-
|
|
577
|
+
/** Per-lens auditor briefs: concrete signals to look for and cross-lens distinctions to respect. @experimental */
|
|
578
|
+
const LENS_BRIEFS = {
|
|
579
|
+
consistency: `
|
|
593
580
|
LENS: consistency
|
|
594
|
-
Look for inconsistencies in the design system
|
|
581
|
+
Look for inconsistencies in the design system — things that look like they came from different products glued together.
|
|
595
582
|
Signals: multiple font families, inconsistent weights/sizes for the same role, two shades of "primary", arbitrary paddings/margins that don't snap to a scale (4/8/12/16/24), same control with different border-radius or shadow on different pages, mixed icon styles (filled vs outlined), inconsistent button heights/padding for the same variant, inconsistent capitalization (Title Case vs sentence case) for the same role.
|
|
596
583
|
NOT this lens: layout misalignment (use \`layout\`), confusing user flow (use \`ux-flow\`), contrast/keyboard issues (use \`accessibility\`).
|
|
597
584
|
Title format: \`Inconsistent <thing> between <A> and <B>\`.
|
|
598
585
|
`.trim(),
|
|
599
|
-
|
|
586
|
+
hierarchy: `
|
|
600
587
|
LENS: hierarchy
|
|
601
|
-
Look for broken visual hierarchy
|
|
588
|
+
Look for broken visual hierarchy — places where the eye does not land on what matters most.
|
|
602
589
|
Signals: primary CTA same weight as secondary/tertiary controls, headings (H1/H2/H3) nearly the same size, important data buried (headline number smaller than its label), decoration outshining content, too many emphases competing, wrong scan order, missing or overly heavy section dividers.
|
|
603
590
|
NOT this lens: same-role styled differently (\`consistency\`), grid/alignment (\`layout\`), contrast-failing text (\`accessibility\`).
|
|
604
591
|
Title format: \`Weak hierarchy: <element> does not read as the <intended-role>\`.
|
|
605
592
|
`.trim(),
|
|
606
|
-
|
|
593
|
+
layout: `
|
|
607
594
|
LENS: layout
|
|
608
|
-
Look for layout and organization problems
|
|
595
|
+
Look for layout and organization problems — alignment, grouping, whitespace, structural choices that hurt scannability.
|
|
609
596
|
Signals: misalignment within rows, inconsistent gutters in grids, orphan whitespace next to crammed regions, poor grouping (related fields separated, unrelated fields adjacent), no visual sections (long wall of content), container overflow (text/content punching out of card boundaries), cramped or oversized hit targets, sidebars/headers sized wrong relative to main content.
|
|
610
597
|
NOT this lens: same-role styled differently (\`consistency\`), click-distance/friction (\`ux-flow\`), overflow specifically at small viewports (\`responsive\`).
|
|
611
598
|
Title format: \`<Region> alignment/spacing problem\` or \`<Region> grouping unclear\`.
|
|
612
599
|
`.trim(),
|
|
613
|
-
|
|
600
|
+
"ux-flow": `
|
|
614
601
|
LENS: ux-flow
|
|
615
|
-
Look for interaction-flow friction
|
|
602
|
+
Look for interaction-flow friction — action sequences that are slower, more annoying, or more error-prone than necessary.
|
|
616
603
|
Signals: sequential clicks far apart (e.g. Next top-right while user is bottom-left), destructive action adjacent to primary with same weight, confirmations that don't say what's being confirmed, primary CTA below the fold or hidden in a kebab menu, silent state changes (toggle gives no feedback), form ordering that fights real-world order, dead-end states after submit, lost inputs on back-navigation, hidden pre-selected options.
|
|
617
604
|
NOT this lens: visual style only (\`consistency\`), component arrangement without a flow problem (\`layout\`), microcopy clarity (\`content\`).
|
|
618
|
-
Title format: \`<Action A>
|
|
605
|
+
Title format: \`<Action A> → <Action B> friction: <root cause>\`.
|
|
619
606
|
`.trim(),
|
|
620
|
-
|
|
607
|
+
duplication: `
|
|
621
608
|
LENS: duplication
|
|
622
|
-
Look for redundancy
|
|
609
|
+
Look for redundancy — the same control, link, or piece of content appearing more than once with no good reason.
|
|
623
610
|
Signals: two ways to do the same action on the same screen with no difference, repeated nav (same links in sidebar AND top nav), drifted duplicates (two copies that have diverged), content repeated verbatim, icon + label saying the same thing twice in one row, per-row + bulk actions that overlap confusingly, multiple status indicators conveying the same status.
|
|
624
|
-
NOT this lens: inconsistent styling of duplicates (\`consistency\`)
|
|
611
|
+
NOT this lens: inconsistent styling of duplicates (\`consistency\`) — this lens is about the existence of duplicates.
|
|
625
612
|
Title format: \`Duplicate <thing> in <location A> and <location B>\`.
|
|
626
613
|
`.trim(),
|
|
627
|
-
|
|
614
|
+
accessibility: `
|
|
628
615
|
LENS: accessibility
|
|
629
|
-
Look for accessibility blockers and degradations. Be conservative
|
|
630
|
-
Signals: insufficient contrast on body text or controls, missing/invisible focus styles, tiny tap targets (<24px on mobile), color as sole signal (red border with no message), form labels missing or not associated (placeholders standing in for labels), broken heading order (H1
|
|
616
|
+
Look for accessibility blockers and degradations. Be conservative — do not assume violations you cannot see.
|
|
617
|
+
Signals: insufficient contrast on body text or controls, missing/invisible focus styles, tiny tap targets (<24px on mobile), color as sole signal (red border with no message), form labels missing or not associated (placeholders standing in for labels), broken heading order (H1 → H4), modals that don't trap focus, decorative elements that take focus, errors not announced, important text rendered inside images.
|
|
631
618
|
NOT this lens: generic "looks confusing" (\`hierarchy\` or \`content\`), layout overflow at small viewports (\`responsive\`).
|
|
632
619
|
Title format: \`Accessibility: <specific blocker> in <element>\`.
|
|
633
620
|
`.trim(),
|
|
634
|
-
|
|
621
|
+
responsive: `
|
|
635
622
|
LENS: responsive
|
|
636
|
-
Look for layout breakage across viewport sizes
|
|
623
|
+
Look for layout breakage across viewport sizes — content that works at one width but degrades at another. This iteration's captures should include the same surface at >=2 viewports; compare across them.
|
|
637
624
|
Signals: horizontal scroll where content should reflow, overlapping elements (header overlaps content, fixed footer covers inputs), desktop nav crammed into mobile without collapsing, table columns that don't truncate, tap targets too close at touch sizes, controls vanishing at certain widths, layout flips that break grouping order, modals exceeding viewport height (confirm button unreachable).
|
|
638
625
|
NOT this lens: issues present at every viewport (\`consistency\` / \`hierarchy\` instead).
|
|
639
626
|
Title format: \`<Element/Region> breaks at <viewport>\`.
|
|
640
627
|
`.trim(),
|
|
641
|
-
|
|
628
|
+
states: `
|
|
642
629
|
LENS: states
|
|
643
|
-
Look for missing or broken UI states
|
|
630
|
+
Look for missing or broken UI states — the not-happy-paths that make a product feel finished or unfinished. The iteration's captures should depict at least one non-default state.
|
|
644
631
|
Signals: empty lists with no guidance, skeletons that don't match final layout (CLS on settle), error states with no message or recovery action, disabled buttons with no explanation, toasts that disappear before being read, success states that don't confirm, missing hover/focus/active/disabled variants on primary controls, no long-content view, no-permission state broken.
|
|
645
632
|
NOT this lens: generic polish on the happy path (other lenses), missing focus rings specifically (\`accessibility\`).
|
|
646
633
|
Title format: \`Missing/broken <state> state on <surface>\`.
|
|
647
634
|
`.trim(),
|
|
648
|
-
|
|
635
|
+
content: `
|
|
649
636
|
LENS: content
|
|
650
|
-
Look for microcopy and content problems
|
|
637
|
+
Look for microcopy and content problems — text that is unclear, inconsistent, condescending, jargon-heavy, or wrong.
|
|
651
638
|
Signals: jargon/internal language leaking ("Provisioning a Tenant" instead of "Setting up your account"), inconsistent terminology (workspace vs team), verbose button labels, empty-state copy that's just "No results", error messages blaming the user, tone inconsistency, truncation without affordance, mixed date/number formats on one page, placeholder used as a label, "Saved!" toast appearing before save completes, typos and grammar errors.
|
|
652
639
|
NOT this lens: visual treatment of text (\`hierarchy\` / \`consistency\`), missing labels for a11y (\`accessibility\`).
|
|
653
640
|
Title format: \`Copy: "<actual text>" in <location>\` or \`Inconsistent term: "<A>" vs "<B>"\`.
|
|
654
641
|
`.trim(),
|
|
655
|
-
|
|
642
|
+
interaction: `
|
|
656
643
|
LENS: interaction
|
|
657
|
-
Look for interaction quality problems
|
|
644
|
+
Look for interaction quality problems — affordances, feedback, and micro-interactions.
|
|
658
645
|
Signals: no affordance (clickable areas not looking clickable, non-clickable areas looking clickable), missing feedback (>100ms click with no progress), hover surprises (whole row highlights but only title clickable), cursor inconsistency, animations that block input, missing transitions where they're needed (accordion snaps open), drag-and-drop without indicators, scroll-jacking, click-through bugs (card click handler firing alongside button), hover-only revelations on touch.
|
|
659
646
|
NOT this lens: position of controls (\`layout\` / \`ux-flow\`), missing focus styles (\`accessibility\`).
|
|
660
647
|
Title format: \`<Action> on <element>: <missing/wrong> feedback\`.
|
|
661
648
|
`.trim(),
|
|
662
|
-
|
|
649
|
+
"performance-perceived": `
|
|
663
650
|
LENS: performance-perceived
|
|
664
|
-
Look for perceived-performance problems
|
|
651
|
+
Look for perceived-performance problems — visible jank a real user would notice, not benchmark numbers. This iteration's captures should include >=2 frames during load to show shift.
|
|
665
652
|
Signals: layout shift (CLS) when late-arriving images/fonts/banners settle, FOUC (flash of unstyled content), font swap jumps, late-loading hero images that shift everything, skeletons that don't match final shape, spinners on instant local actions, loading state reappearing after content paints (refetch on focus), modal open animation longer than the operation it precedes.
|
|
666
653
|
NOT this lens: slow API calls (file separately), stale data after navigation (\`states\`).
|
|
667
654
|
Title format: \`Layout shift / late paint on <route>: <root cause>\`.
|
|
668
655
|
`.trim(),
|
|
669
|
-
|
|
656
|
+
other: `
|
|
670
657
|
LENS: other
|
|
671
|
-
Use ONLY when a finding is clearly a UI quality issue but does not fit any other lens. Strongly prefer a specific lens
|
|
658
|
+
Use ONLY when a finding is clearly a UI quality issue but does not fit any other lens. Strongly prefer a specific lens — \`other\` should be rare. Title must still be concrete.
|
|
672
659
|
`.trim()
|
|
673
660
|
};
|
|
661
|
+
/**
|
|
662
|
+
* Build a system prompt for a single auditor iteration.
|
|
663
|
+
*
|
|
664
|
+
* @experimental
|
|
665
|
+
*/
|
|
674
666
|
function buildAuditorSystemPrompt(lens) {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
${brief}`;
|
|
667
|
+
const brief = LENS_BRIEFS[lens];
|
|
668
|
+
return `${SHARED_AUDITOR_RULES}\n\n${brief}`;
|
|
679
669
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
670
|
+
//#endregion
|
|
671
|
+
//#region src/profiles/ui-auditor/output-adapter.ts
|
|
672
|
+
const KNOWN_LENS_VALUES = new Set(UI_LENSES);
|
|
683
673
|
function isUiLens(v) {
|
|
684
|
-
|
|
674
|
+
return typeof v === "string" && KNOWN_LENS_VALUES.has(v);
|
|
685
675
|
}
|
|
676
|
+
/** Parse raw `SandboxEvent` emissions from an audit iteration into structured `UiAuditOutput`. @experimental */
|
|
686
677
|
function parseAuditorEvents(events) {
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
678
|
+
const findings = [];
|
|
679
|
+
const captures = [];
|
|
680
|
+
let lens;
|
|
681
|
+
let notes;
|
|
682
|
+
for (const evt of events) {
|
|
683
|
+
if (!evt || typeof evt !== "object") continue;
|
|
684
|
+
const type = String(evt.type ?? "");
|
|
685
|
+
const data = evt.data && typeof evt.data === "object" ? evt.data : void 0;
|
|
686
|
+
if (!data) continue;
|
|
687
|
+
switch (type) {
|
|
688
|
+
case "audit.lens": {
|
|
689
|
+
const v = data.lens;
|
|
690
|
+
if (isUiLens(v)) lens = v;
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
case "audit.capture": {
|
|
694
|
+
const cap = data;
|
|
695
|
+
if (typeof cap.path === "string" && typeof cap.viewport === "string" && typeof cap.fullPage === "boolean" && typeof cap.route === "string" && typeof cap.url === "string" && typeof cap.capturedAt === "string") {
|
|
696
|
+
const out = {
|
|
697
|
+
path: cap.path,
|
|
698
|
+
viewport: cap.viewport,
|
|
699
|
+
fullPage: cap.fullPage,
|
|
700
|
+
route: cap.route,
|
|
701
|
+
url: cap.url,
|
|
702
|
+
capturedAt: cap.capturedAt
|
|
703
|
+
};
|
|
704
|
+
if (cap.elementSelector) out.elementSelector = cap.elementSelector;
|
|
705
|
+
if (cap.label) out.label = cap.label;
|
|
706
|
+
captures.push(out);
|
|
707
|
+
}
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
case "audit.finding": {
|
|
711
|
+
const f = data;
|
|
712
|
+
if (typeof f.title === "string" && f.title.trim().length > 0 && isUiLens(f.lens) && typeof f.severity === "string" && [
|
|
713
|
+
"low",
|
|
714
|
+
"med",
|
|
715
|
+
"high",
|
|
716
|
+
"critical"
|
|
717
|
+
].includes(f.severity) && typeof f.route === "string" && typeof f.observation === "string" && typeof f.impact === "string" && typeof f.suggestedFix === "string" && Array.isArray(f.screenshots)) findings.push(f);
|
|
718
|
+
break;
|
|
719
|
+
}
|
|
720
|
+
case "audit.notes": {
|
|
721
|
+
const n = data.notes;
|
|
722
|
+
if (typeof n === "string" && n.trim().length > 0) notes = n;
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
default: break;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
const out = {
|
|
729
|
+
lens: lens ?? "other",
|
|
730
|
+
findings,
|
|
731
|
+
captures
|
|
732
|
+
};
|
|
733
|
+
if (notes) out.notes = notes;
|
|
734
|
+
return out;
|
|
735
|
+
}
|
|
736
|
+
//#endregion
|
|
737
|
+
//#region src/profiles/ui-auditor/validator.ts
|
|
738
|
+
const GENERIC_TITLE_PATTERNS = [
|
|
739
|
+
/^improve\s/i,
|
|
740
|
+
/^fix\s/i,
|
|
741
|
+
/^update\s/i,
|
|
742
|
+
/^better\s/i,
|
|
743
|
+
/^bad\s/i,
|
|
744
|
+
/^make\s.+\sbetter/i,
|
|
745
|
+
/\bUX\b\s*$/i,
|
|
746
|
+
/\bUI\b\s*$/i
|
|
750
747
|
];
|
|
751
748
|
function isGenericTitle(title) {
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
749
|
+
const t = title.trim();
|
|
750
|
+
if (t.length < 16) return true;
|
|
751
|
+
return GENERIC_TITLE_PATTERNS.some((re) => re.test(t));
|
|
755
752
|
}
|
|
753
|
+
/** Build a `Validator` that rejects off-lens findings and findings missing screenshot evidence. @experimental */
|
|
756
754
|
function createUiAuditorValidator(task) {
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
const verdict = {
|
|
815
|
-
valid: true,
|
|
816
|
-
score,
|
|
817
|
-
notes: `${findings.length} finding(s) \u2014 specificity=${specificity.toFixed(2)} evidence=${evidence.toFixed(2)} titles=${titles.toFixed(2)}`,
|
|
818
|
-
scores: { specificity, evidence, titles }
|
|
819
|
-
};
|
|
820
|
-
return verdict;
|
|
821
|
-
}
|
|
822
|
-
};
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
// src/profiles/ui-auditor/profile.ts
|
|
755
|
+
return { async validate(output) {
|
|
756
|
+
const findings = output.findings;
|
|
757
|
+
const captures = output.captures;
|
|
758
|
+
const capturePaths = new Set(captures.map((c) => c.path));
|
|
759
|
+
const offLens = findings.filter((f) => f.lens !== task.lens);
|
|
760
|
+
if (offLens.length > 0) return {
|
|
761
|
+
valid: false,
|
|
762
|
+
score: 0,
|
|
763
|
+
notes: `${offLens.length} finding(s) filed under wrong lens (expected ${task.lens}; got ${offLens.map((f) => f.lens).join(", ")})`,
|
|
764
|
+
scores: { offLens: 0 }
|
|
765
|
+
};
|
|
766
|
+
const missingEvidence = findings.filter((f) => !Array.isArray(f.screenshots) || f.screenshots.length === 0);
|
|
767
|
+
if (missingEvidence.length > 0) return {
|
|
768
|
+
valid: false,
|
|
769
|
+
score: 0,
|
|
770
|
+
notes: `${missingEvidence.length} finding(s) have no screenshot evidence`,
|
|
771
|
+
scores: { evidence: 0 }
|
|
772
|
+
};
|
|
773
|
+
const unresolvedShot = findings.filter((f) => f.screenshots.some((s) => !capturePaths.has(s.path)));
|
|
774
|
+
if (unresolvedShot.length > 0) return {
|
|
775
|
+
valid: false,
|
|
776
|
+
score: 0,
|
|
777
|
+
notes: `${unresolvedShot.length} finding(s) reference screenshot paths not captured this iteration`,
|
|
778
|
+
scores: { evidence: 0 }
|
|
779
|
+
};
|
|
780
|
+
if (findings.length === 0) return {
|
|
781
|
+
valid: true,
|
|
782
|
+
score: .5,
|
|
783
|
+
notes: "No findings reported. Neither a confident pass nor a failure.",
|
|
784
|
+
scores: {
|
|
785
|
+
specificity: 0,
|
|
786
|
+
evidence: 1,
|
|
787
|
+
titles: 1
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
const specificity = findings.filter((f) => typeof f.selector === "string").length / findings.length;
|
|
791
|
+
const titles = 1 - findings.filter((f) => isGenericTitle(f.title)).length / findings.length;
|
|
792
|
+
const evidence = findings.filter((f) => Array.isArray(f.screenshots) && f.screenshots.length > 0 && f.screenshots.every((s) => capturePaths.has(s.path))).length / findings.length;
|
|
793
|
+
return {
|
|
794
|
+
valid: true,
|
|
795
|
+
score: Number((.4 * specificity + .4 * evidence + .2 * titles).toFixed(4)),
|
|
796
|
+
notes: `${findings.length} finding(s) — specificity=${specificity.toFixed(2)} evidence=${evidence.toFixed(2)} titles=${titles.toFixed(2)}`,
|
|
797
|
+
scores: {
|
|
798
|
+
specificity,
|
|
799
|
+
evidence,
|
|
800
|
+
titles
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
} };
|
|
804
|
+
}
|
|
805
|
+
//#endregion
|
|
806
|
+
//#region src/profiles/ui-auditor/profile.ts
|
|
807
|
+
/**
|
|
808
|
+
* Preset `runAgentRounds` bundle for vision-driven UI audits: returns the `AgentRunSpec`, output adapter, validator, and prompt formatter the loop kernel needs.
|
|
809
|
+
*
|
|
810
|
+
* @experimental
|
|
811
|
+
*/
|
|
826
812
|
function uiAuditorProfile(options = {}) {
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
813
|
+
const name = options.name ?? "ui-auditor";
|
|
814
|
+
const profile = {
|
|
815
|
+
name,
|
|
816
|
+
description: "Vision-driven UI auditor. One lens per iteration.",
|
|
817
|
+
prompt: { systemPrompt: "" },
|
|
818
|
+
model: options.model ? { default: options.model } : void 0,
|
|
819
|
+
tools: {
|
|
820
|
+
browser: true,
|
|
821
|
+
vision: true
|
|
822
|
+
},
|
|
823
|
+
metadata: { role: "ui-auditor" }
|
|
824
|
+
};
|
|
825
|
+
const output = { parse: parseAuditorEvents };
|
|
826
|
+
const validator = options.task ? createUiAuditorValidator(options.task) : createUiAuditorValidator({
|
|
827
|
+
lens: "other",
|
|
828
|
+
captures: []
|
|
829
|
+
});
|
|
830
|
+
const taskToPrompt = (task) => `${encodeAuditTaskEnvelope(task)}\n${buildAuditorSystemPrompt(task.lens)}\n\n${formatAuditorPrompt(task)}`;
|
|
831
|
+
return {
|
|
832
|
+
profile,
|
|
833
|
+
taskToPrompt,
|
|
834
|
+
output,
|
|
835
|
+
validator,
|
|
836
|
+
agentRunSpec: {
|
|
837
|
+
name,
|
|
838
|
+
profile,
|
|
839
|
+
taskToPrompt
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
//#endregion
|
|
844
|
+
export { LENS_BRIEFS, SHARED_AUDITOR_RULES, UI_FINDING_SEVERITIES, UI_LENSES, appendFindings, buildAuditorSystemPrompt, coderTaskToPrompt, createInProcessUiAuditClient, createResearcherValidator, createUiAuditorValidator, decodeAuditTaskEnvelope, encodeAuditTaskEnvelope, formatAuditorPrompt, initAuditWorkspace, multiHarnessResearcherFanout, parseAuditorEvents, readAuditRegistry, registerCaptures, researcherProfile, summarizeRegistry, uiAuditorProfile, writeAuditIndex };
|
|
840
845
|
|
|
841
|
-
${formatAuditorPrompt(task)}`;
|
|
842
|
-
const agentRunSpec = {
|
|
843
|
-
name,
|
|
844
|
-
profile,
|
|
845
|
-
taskToPrompt
|
|
846
|
-
};
|
|
847
|
-
return { profile, taskToPrompt, output, validator, agentRunSpec };
|
|
848
|
-
}
|
|
849
|
-
export {
|
|
850
|
-
LENS_BRIEFS,
|
|
851
|
-
SHARED_AUDITOR_RULES,
|
|
852
|
-
UI_FINDING_SEVERITIES,
|
|
853
|
-
UI_LENSES,
|
|
854
|
-
appendFindings,
|
|
855
|
-
buildAuditorSystemPrompt,
|
|
856
|
-
coderTaskToPrompt,
|
|
857
|
-
createInProcessUiAuditClient,
|
|
858
|
-
createResearcherValidator,
|
|
859
|
-
createUiAuditorValidator,
|
|
860
|
-
decodeAuditTaskEnvelope,
|
|
861
|
-
encodeAuditTaskEnvelope,
|
|
862
|
-
formatAuditorPrompt,
|
|
863
|
-
initAuditWorkspace,
|
|
864
|
-
multiHarnessResearcherFanout,
|
|
865
|
-
parseAuditorEvents,
|
|
866
|
-
readAuditRegistry,
|
|
867
|
-
registerCaptures,
|
|
868
|
-
researcherProfile,
|
|
869
|
-
summarizeRegistry,
|
|
870
|
-
uiAuditorProfile,
|
|
871
|
-
writeAuditIndex
|
|
872
|
-
};
|
|
873
846
|
//# sourceMappingURL=profiles.js.map
|