@tangle-network/agent-runtime 0.43.0 → 0.45.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 +96 -202
- package/dist/agent.d.ts +5 -4
- package/dist/agent.js +5 -7
- package/dist/agent.js.map +1 -1
- package/dist/analyst-loop.d.ts +65 -4
- package/dist/analyst-loop.js +6 -1
- package/dist/audit.d.ts +93 -0
- package/dist/audit.js +312 -0
- package/dist/audit.js.map +1 -0
- package/dist/chunk-4B6U4CVQ.js +15 -0
- package/dist/chunk-4B6U4CVQ.js.map +1 -0
- package/dist/chunk-FK53TXOP.js +603 -0
- package/dist/chunk-FK53TXOP.js.map +1 -0
- package/dist/{chunk-MJDGCRAT.js → chunk-IJ6FGOPO.js} +5 -5
- package/dist/chunk-IJ6FGOPO.js.map +1 -0
- package/dist/{chunk-HVYOHJHK.js → chunk-IJGS6J7X.js} +2 -2
- package/dist/chunk-IJGS6J7X.js.map +1 -0
- package/dist/chunk-KEWO4KI6.js +3599 -0
- package/dist/chunk-KEWO4KI6.js.map +1 -0
- package/dist/{chunk-NRZOXCJK.js → chunk-KSMX62JF.js} +2 -2
- package/dist/{chunk-C5HMTTNY.js → chunk-NYN5RTLP.js} +13 -12
- package/dist/chunk-NYN5RTLP.js.map +1 -0
- package/dist/chunk-PRX45WE2.js +264 -0
- package/dist/chunk-PRX45WE2.js.map +1 -0
- package/dist/{chunk-3HMHSN22.js → chunk-QR4UUC5P.js} +6 -6
- package/dist/chunk-QR4UUC5P.js.map +1 -0
- package/dist/chunk-WIR4HOOJ.js +27 -0
- package/dist/chunk-WIR4HOOJ.js.map +1 -0
- package/dist/{chunk-MNCB4SJ5.js → chunk-Z2QXVBA6.js} +296 -8
- package/dist/chunk-Z2QXVBA6.js.map +1 -0
- package/dist/coder-CczgMqFx.d.ts +114 -0
- package/dist/dynamic-BvllHV6M.d.ts +221 -0
- package/dist/{improvement-adapter-BC4HhuAR.d.ts → improvement-adapter-CWegd3vw.d.ts} +1 -1
- package/dist/improvement.d.ts +2 -3
- package/dist/improvement.js +0 -5
- package/dist/improvement.js.map +1 -1
- package/dist/index.d.ts +123 -10
- package/dist/index.js +407 -19
- package/dist/index.js.map +1 -1
- package/dist/{kb-gate-DTBum3vH.d.ts → kb-gate-D9GBocLN.d.ts} +82 -5
- package/dist/{loop-runner-bin-CVoCBmYk.d.ts → loop-runner-bin-CPrCoKqC.d.ts} +14 -10
- package/dist/loop-runner-bin.d.ts +9 -7
- package/dist/loop-runner-bin.js +6 -8
- package/dist/loops.d.ts +7 -371
- package/dist/loops.js +96 -19
- package/dist/mcp/bin.js +7 -7
- package/dist/mcp/bin.js.map +1 -1
- package/dist/mcp/index.d.ts +284 -11
- package/dist/mcp/index.js +341 -9
- package/dist/mcp/index.js.map +1 -1
- package/dist/{otel-export-BzvF1Ela.d.ts → otel-export-Dy2DyUCU.d.ts} +1 -1
- package/dist/profiles.d.ts +385 -86
- package/dist/profiles.js +549 -4
- package/dist/profiles.js.map +1 -1
- package/dist/run-loop--hSoIknW.d.ts +112 -0
- package/dist/runtime-hooks-C7JwKb9E.d.ts +70 -0
- package/dist/runtime.d.ts +1860 -0
- package/dist/runtime.js +114 -0
- package/dist/runtime.js.map +1 -0
- package/dist/substrate-CUgk7F7s.d.ts +77 -0
- package/dist/topology.d.ts +73 -0
- package/dist/topology.js +111 -0
- package/dist/topology.js.map +1 -0
- package/dist/types-1HbsFa7H.d.ts +438 -0
- package/dist/{types-p8dWBIXL.d.ts → types-BtRLF2U3.d.ts} +1 -1
- package/dist/{types-Bcp071Jg.d.ts → types-DdzkffAm.d.ts} +95 -1
- package/dist/workflow.d.ts +551 -0
- package/dist/workflow.js +1778 -0
- package/dist/workflow.js.map +1 -0
- package/package.json +53 -16
- package/skills/agent-runtime-adoption/SKILL.md +29 -26
- package/dist/chunk-3HMHSN22.js.map +0 -1
- package/dist/chunk-C5HMTTNY.js.map +0 -1
- package/dist/chunk-EKBSQYZE.js +0 -813
- package/dist/chunk-EKBSQYZE.js.map +0 -1
- package/dist/chunk-HVYOHJHK.js.map +0 -1
- package/dist/chunk-MJDGCRAT.js.map +0 -1
- package/dist/chunk-MNCB4SJ5.js.map +0 -1
- package/dist/chunk-PY6NMZYX.js +0 -52
- package/dist/chunk-PY6NMZYX.js.map +0 -1
- package/dist/chunk-SQSCRJ7U.js +0 -65
- package/dist/chunk-SQSCRJ7U.js.map +0 -1
- package/dist/chunk-VOX6Z3II.js +0 -90
- package/dist/chunk-VOX6Z3II.js.map +0 -1
- package/dist/chunk-XBUG326M.js +0 -261
- package/dist/chunk-XBUG326M.js.map +0 -1
- package/dist/dynamic-B_7GgCwu.d.ts +0 -108
- package/dist/optimize-prompt-D-urF2wW.d.ts +0 -129
- /package/dist/{chunk-NRZOXCJK.js.map → chunk-KSMX62JF.js.map} +0 -0
package/dist/audit.js
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import {
|
|
2
|
+
slugify
|
|
3
|
+
} from "./chunk-4B6U4CVQ.js";
|
|
4
|
+
import {
|
|
5
|
+
UI_FINDING_SEVERITIES,
|
|
6
|
+
UI_LENSES
|
|
7
|
+
} from "./chunk-WIR4HOOJ.js";
|
|
8
|
+
import "./chunk-DGUM43GV.js";
|
|
9
|
+
|
|
10
|
+
// src/audit/issue-writer.ts
|
|
11
|
+
import { promises as fs } from "fs";
|
|
12
|
+
import path from "path";
|
|
13
|
+
var SEVERITY_ORDER = {
|
|
14
|
+
critical: 0,
|
|
15
|
+
high: 1,
|
|
16
|
+
med: 2,
|
|
17
|
+
low: 3
|
|
18
|
+
};
|
|
19
|
+
function assertValidWorkspaceDir(dir) {
|
|
20
|
+
if (typeof dir !== "string" || dir.length === 0) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`audit-writer: workspaceDir must be a non-empty string (got ${JSON.stringify(dir)})`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
if (!path.isAbsolute(dir)) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`audit-writer: workspaceDir must be an absolute path (got ${JSON.stringify(dir)})`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
if (dir.split(path.sep).includes("..")) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`audit-writer: workspaceDir must not contain '..' segments (got ${JSON.stringify(dir)})`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function initAuditWorkspace(workspaceDir) {
|
|
37
|
+
assertValidWorkspaceDir(workspaceDir);
|
|
38
|
+
await fs.mkdir(path.join(workspaceDir, "issues"), { recursive: true });
|
|
39
|
+
await fs.mkdir(path.join(workspaceDir, "screenshots"), { recursive: true });
|
|
40
|
+
const regPath = path.join(workspaceDir, "registry.json");
|
|
41
|
+
try {
|
|
42
|
+
await fs.access(regPath);
|
|
43
|
+
} catch {
|
|
44
|
+
const fresh = { schemaVersion: 1, findings: [], routes: {} };
|
|
45
|
+
await fs.writeFile(regPath, JSON.stringify(fresh, null, 2));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function readAuditRegistry(workspaceDir) {
|
|
49
|
+
assertValidWorkspaceDir(workspaceDir);
|
|
50
|
+
const regPath = path.join(workspaceDir, "registry.json");
|
|
51
|
+
const text = await fs.readFile(regPath, "utf8");
|
|
52
|
+
const parsed = JSON.parse(text);
|
|
53
|
+
if (!parsed || typeof parsed !== "object") {
|
|
54
|
+
throw new Error(`audit-writer: registry.json at ${regPath} is not a JSON object`);
|
|
55
|
+
}
|
|
56
|
+
const reg = parsed;
|
|
57
|
+
if (reg.schemaVersion !== 1 || !Array.isArray(reg.findings) || typeof reg.routes !== "object") {
|
|
58
|
+
throw new Error(`audit-writer: registry.json at ${regPath} has an unrecognized shape`);
|
|
59
|
+
}
|
|
60
|
+
return reg;
|
|
61
|
+
}
|
|
62
|
+
async function writeAuditRegistry(workspaceDir, reg) {
|
|
63
|
+
const regPath = path.join(workspaceDir, "registry.json");
|
|
64
|
+
const tmpPath = `${regPath}.tmp-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
65
|
+
await fs.writeFile(tmpPath, JSON.stringify(reg, null, 2));
|
|
66
|
+
await fs.rename(tmpPath, regPath);
|
|
67
|
+
}
|
|
68
|
+
var workspaceLocks = /* @__PURE__ */ new Map();
|
|
69
|
+
async function withWorkspaceLock(workspaceDir, fn) {
|
|
70
|
+
const key = path.resolve(workspaceDir);
|
|
71
|
+
const prev = workspaceLocks.get(key) ?? Promise.resolve();
|
|
72
|
+
let signalDone;
|
|
73
|
+
const done = new Promise((resolve) => {
|
|
74
|
+
signalDone = resolve;
|
|
75
|
+
});
|
|
76
|
+
workspaceLocks.set(key, done);
|
|
77
|
+
try {
|
|
78
|
+
await prev;
|
|
79
|
+
return await fn();
|
|
80
|
+
} finally {
|
|
81
|
+
signalDone();
|
|
82
|
+
if (workspaceLocks.get(key) === done) workspaceLocks.delete(key);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function assertFindingShape(f, index) {
|
|
86
|
+
const where = `audit-writer: findings[${index}]`;
|
|
87
|
+
if (!UI_LENSES.includes(f.lens)) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`${where}: invalid lens ${JSON.stringify(f.lens)}; one of ${UI_LENSES.join("|")}`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (!UI_FINDING_SEVERITIES.includes(f.severity)) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`${where}: invalid severity ${JSON.stringify(f.severity)}; one of ${UI_FINDING_SEVERITIES.join("|")}`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
for (const field of ["title", "route", "observation", "impact", "suggestedFix"]) {
|
|
98
|
+
const v = f[field];
|
|
99
|
+
if (typeof v !== "string" || v.trim().length === 0) {
|
|
100
|
+
throw new Error(`${where}: ${field} must be a non-empty string`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (!Array.isArray(f.screenshots) || f.screenshots.length === 0) {
|
|
104
|
+
throw new Error(`${where}: screenshots must be a non-empty array`);
|
|
105
|
+
}
|
|
106
|
+
for (let i = 0; i < f.screenshots.length; i += 1) {
|
|
107
|
+
const s = f.screenshots[i];
|
|
108
|
+
if (!s || typeof s.path !== "string" || s.path.length === 0) {
|
|
109
|
+
throw new Error(`${where}.screenshots[${i}].path must be a non-empty string`);
|
|
110
|
+
}
|
|
111
|
+
if (s.path.split(/[/\\]/).includes("..")) {
|
|
112
|
+
throw new Error(`${where}.screenshots[${i}].path must not contain '..' segments`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function nextFindingId(reg) {
|
|
117
|
+
let max = 0;
|
|
118
|
+
for (const f of reg.findings) {
|
|
119
|
+
if (typeof f.id === "number" && f.id > max) max = f.id;
|
|
120
|
+
}
|
|
121
|
+
return max + 1;
|
|
122
|
+
}
|
|
123
|
+
function slugifyTitle(title) {
|
|
124
|
+
return slugify(title, "title");
|
|
125
|
+
}
|
|
126
|
+
function renderFinding(finding) {
|
|
127
|
+
if (!finding.id) {
|
|
128
|
+
throw new Error("audit-writer: cannot render a finding without an assigned id");
|
|
129
|
+
}
|
|
130
|
+
if (finding.screenshots.length === 0) {
|
|
131
|
+
throw new Error(`audit-writer: finding #${finding.id} has no screenshots`);
|
|
132
|
+
}
|
|
133
|
+
const id = finding.id;
|
|
134
|
+
const numStr = String(id).padStart(3, "0");
|
|
135
|
+
const metaLines = [
|
|
136
|
+
`> **Issue #${numStr}** \xB7 **Severity:** ${finding.severity} \xB7 **Lens:** ${finding.lens}`,
|
|
137
|
+
`> **Route:** \`${finding.route}\`${finding.url ? ` \xB7 **URL:** ${finding.url}` : ""}`
|
|
138
|
+
];
|
|
139
|
+
if (finding.viewport) metaLines.push(`> **Viewport:** ${finding.viewport}`);
|
|
140
|
+
if (finding.selector) metaLines.push(`> **Selector:** \`${finding.selector}\``);
|
|
141
|
+
if (finding.tags && finding.tags.length > 0) {
|
|
142
|
+
metaLines.push(`> **Tags:** ${finding.tags.map((t) => `\`${t}\``).join(", ")}`);
|
|
143
|
+
}
|
|
144
|
+
if (finding.similarTo && finding.similarTo.length > 0) {
|
|
145
|
+
metaLines.push(
|
|
146
|
+
`> **Possible duplicates:** ${finding.similarTo.map((n) => `#${String(n).padStart(3, "0")}`).join(", ")}`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
metaLines.push(`> _Generated: ${finding.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()}_`);
|
|
150
|
+
const reproSteps = finding.reproSteps ?? `1. Open \`${finding.route}\`${finding.url ? ` (${finding.url})` : ""}
|
|
151
|
+
2. Observe the area shown in the screenshot${finding.selector ? ` (selector: \`${finding.selector}\`)` : ""}.`;
|
|
152
|
+
const evidence = finding.screenshots.map((s) => ``).join("\n\n");
|
|
153
|
+
return [
|
|
154
|
+
`# [UI] ${finding.title}`,
|
|
155
|
+
"",
|
|
156
|
+
metaLines.join("\n"),
|
|
157
|
+
"",
|
|
158
|
+
"## Observation",
|
|
159
|
+
finding.observation.trim(),
|
|
160
|
+
"",
|
|
161
|
+
"## Why it matters",
|
|
162
|
+
finding.impact.trim(),
|
|
163
|
+
"",
|
|
164
|
+
"## Steps to reproduce",
|
|
165
|
+
reproSteps,
|
|
166
|
+
"",
|
|
167
|
+
"## Suggested fix",
|
|
168
|
+
finding.suggestedFix.trim(),
|
|
169
|
+
"",
|
|
170
|
+
"## Evidence",
|
|
171
|
+
evidence,
|
|
172
|
+
"",
|
|
173
|
+
"---",
|
|
174
|
+
`<sub>Generated by agent-runtime/ui-auditor \xB7 lens=\`${finding.lens}\` \xB7 severity=\`${finding.severity}\`</sub>`,
|
|
175
|
+
""
|
|
176
|
+
].join("\n");
|
|
177
|
+
}
|
|
178
|
+
async function appendFindings(workspaceDir, findings) {
|
|
179
|
+
assertValidWorkspaceDir(workspaceDir);
|
|
180
|
+
for (let i = 0; i < findings.length; i += 1) {
|
|
181
|
+
const f = findings[i];
|
|
182
|
+
if (!f) throw new Error(`audit-writer: findings[${i}] is undefined`);
|
|
183
|
+
assertFindingShape(f, i);
|
|
184
|
+
}
|
|
185
|
+
return withWorkspaceLock(workspaceDir, async () => {
|
|
186
|
+
await initAuditWorkspace(workspaceDir);
|
|
187
|
+
const reg = await readAuditRegistry(workspaceDir);
|
|
188
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
189
|
+
for (const f of reg.findings) {
|
|
190
|
+
if (typeof f.id === "number") usedIds.add(f.id);
|
|
191
|
+
}
|
|
192
|
+
const written = [];
|
|
193
|
+
const files = [];
|
|
194
|
+
let nextId = nextFindingId(reg);
|
|
195
|
+
for (const incoming of findings) {
|
|
196
|
+
let id = incoming.id;
|
|
197
|
+
if (id !== void 0) {
|
|
198
|
+
if (usedIds.has(id)) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`audit-writer: incoming finding id ${id} (title=${JSON.stringify(incoming.title)}) collides with an existing registry entry`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
id = nextId;
|
|
205
|
+
while (usedIds.has(id)) id += 1;
|
|
206
|
+
nextId = id + 1;
|
|
207
|
+
}
|
|
208
|
+
usedIds.add(id);
|
|
209
|
+
const createdAt = incoming.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
210
|
+
const persisted = { ...incoming, id, createdAt };
|
|
211
|
+
const slug = slugifyTitle(persisted.title);
|
|
212
|
+
const fileName = `${String(id).padStart(3, "0")}--${persisted.lens}--${slug}.md`;
|
|
213
|
+
const filePathAbs = path.join(workspaceDir, "issues", fileName);
|
|
214
|
+
const filePathRel = `issues/${fileName}`;
|
|
215
|
+
await fs.writeFile(filePathAbs, renderFinding(persisted));
|
|
216
|
+
reg.findings.push(persisted);
|
|
217
|
+
written.push(persisted);
|
|
218
|
+
files.push(filePathRel);
|
|
219
|
+
}
|
|
220
|
+
await writeAuditRegistry(workspaceDir, reg);
|
|
221
|
+
return { written, files };
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
async function registerCaptures(workspaceDir, options) {
|
|
225
|
+
assertValidWorkspaceDir(workspaceDir);
|
|
226
|
+
if (typeof options.route !== "string" || options.route.trim().length === 0) {
|
|
227
|
+
throw new Error("audit-writer: registerCaptures: route must be a non-empty string");
|
|
228
|
+
}
|
|
229
|
+
return withWorkspaceLock(workspaceDir, async () => {
|
|
230
|
+
await initAuditWorkspace(workspaceDir);
|
|
231
|
+
const reg = await readAuditRegistry(workspaceDir);
|
|
232
|
+
const slot = reg.routes[options.route] ?? { captures: [] };
|
|
233
|
+
if (options.url) slot.url = options.url;
|
|
234
|
+
slot.captures = [...slot.captures, ...options.captures];
|
|
235
|
+
reg.routes[options.route] = slot;
|
|
236
|
+
await writeAuditRegistry(workspaceDir, reg);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
function summarizeRegistry(reg) {
|
|
240
|
+
const bySeverity = {
|
|
241
|
+
critical: 0,
|
|
242
|
+
high: 0,
|
|
243
|
+
med: 0,
|
|
244
|
+
low: 0
|
|
245
|
+
};
|
|
246
|
+
const byLens = {};
|
|
247
|
+
const byRoute = {};
|
|
248
|
+
for (const f of reg.findings) {
|
|
249
|
+
bySeverity[f.severity] += 1;
|
|
250
|
+
byLens[f.lens] = (byLens[f.lens] ?? 0) + 1;
|
|
251
|
+
byRoute[f.route] = (byRoute[f.route] ?? 0) + 1;
|
|
252
|
+
}
|
|
253
|
+
return { total: reg.findings.length, bySeverity, byLens, byRoute };
|
|
254
|
+
}
|
|
255
|
+
async function writeAuditIndex(workspaceDir) {
|
|
256
|
+
assertValidWorkspaceDir(workspaceDir);
|
|
257
|
+
const reg = await readAuditRegistry(workspaceDir);
|
|
258
|
+
const summary = summarizeRegistry(reg);
|
|
259
|
+
const sorted = [...reg.findings].sort((a, b) => {
|
|
260
|
+
const sa = SEVERITY_ORDER[a.severity];
|
|
261
|
+
const sb = SEVERITY_ORDER[b.severity];
|
|
262
|
+
if (sa !== sb) return sa - sb;
|
|
263
|
+
return (a.id ?? 0) - (b.id ?? 0);
|
|
264
|
+
});
|
|
265
|
+
const lines = [];
|
|
266
|
+
lines.push("# UI audit index", "");
|
|
267
|
+
lines.push(`_Generated: ${(/* @__PURE__ */ new Date()).toISOString()}_`, "");
|
|
268
|
+
lines.push(`**Total findings:** ${summary.total}`, "");
|
|
269
|
+
lines.push("## By severity");
|
|
270
|
+
for (const s of ["critical", "high", "med", "low"]) {
|
|
271
|
+
const n = summary.bySeverity[s] ?? 0;
|
|
272
|
+
if (n > 0) lines.push(`- **${s}** \u2014 ${n}`);
|
|
273
|
+
}
|
|
274
|
+
lines.push("", "## By lens");
|
|
275
|
+
const lensEntries = Object.entries(summary.byLens).map(([k, v]) => [
|
|
276
|
+
k,
|
|
277
|
+
v ?? 0
|
|
278
|
+
]);
|
|
279
|
+
for (const [lens, n] of lensEntries.sort((a, b) => b[1] - a[1])) {
|
|
280
|
+
lines.push(`- **${lens}** \u2014 ${n}`);
|
|
281
|
+
}
|
|
282
|
+
lines.push("", "## By route");
|
|
283
|
+
const routeEntries = Object.entries(summary.byRoute).map(([k, v]) => [
|
|
284
|
+
k,
|
|
285
|
+
v ?? 0
|
|
286
|
+
]);
|
|
287
|
+
for (const [route, n] of routeEntries.sort((a, b) => b[1] - a[1])) {
|
|
288
|
+
lines.push(`- \`${route}\` \u2014 ${n}`);
|
|
289
|
+
}
|
|
290
|
+
lines.push("", "## Findings", "");
|
|
291
|
+
for (const f of sorted) {
|
|
292
|
+
if (typeof f.id !== "number") continue;
|
|
293
|
+
const num = String(f.id).padStart(3, "0");
|
|
294
|
+
const slug = slugifyTitle(f.title);
|
|
295
|
+
const file = `issues/${num}--${f.lens}--${slug}.md`;
|
|
296
|
+
lines.push(`- [#${num} \`${f.severity}\` \`${f.lens}\` \`${f.route}\` \u2014 ${f.title}](${file})`);
|
|
297
|
+
}
|
|
298
|
+
lines.push("");
|
|
299
|
+
const out = path.join(workspaceDir, "index.md");
|
|
300
|
+
const body = lines.join("\n");
|
|
301
|
+
await fs.writeFile(out, body);
|
|
302
|
+
return out;
|
|
303
|
+
}
|
|
304
|
+
export {
|
|
305
|
+
appendFindings,
|
|
306
|
+
initAuditWorkspace,
|
|
307
|
+
readAuditRegistry,
|
|
308
|
+
registerCaptures,
|
|
309
|
+
summarizeRegistry,
|
|
310
|
+
writeAuditIndex
|
|
311
|
+
};
|
|
312
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/audit/issue-writer.ts"],"sourcesContent":["/**\n * @experimental\n *\n * UI-audit issue writer — pure I/O. Takes a workspace dir + `UiFinding[]`\n * and emits:\n * - `<workspace>/issues/NNN--<lens>--<slug>.md` — one self-contained\n * GitHub-issue-ready Markdown per finding, with embedded screenshot\n * references.\n * - `<workspace>/registry.json` — finding index for dedup and audit\n * resume across iterations.\n * - `<workspace>/index.md` — human-readable rollup (severity / lens /\n * route counts plus a sorted finding list).\n *\n * The writer is deterministic, idempotent for `appendFindings()`, and\n * never invokes an LLM. It assigns the next monotonic id to a finding the\n * caller did not pre-id.\n */\n\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\nimport { slugify } from '../profiles/ui-auditor/slugify'\nimport {\n UI_FINDING_SEVERITIES,\n UI_LENSES,\n type UiFinding,\n type UiLens,\n} from '../profiles/ui-auditor/substrate'\n\n/** @experimental */\nexport interface AuditRegistry {\n schemaVersion: 1\n findings: UiFinding[]\n /** Route → URL + captures sidecar; preserved across runs. */\n routes: Record<string, { url?: string; captures: AuditRegistryCapture[] }>\n}\n\n/** @experimental */\nexport interface AuditRegistryCapture {\n file: string\n viewport?: string\n fullPage?: boolean\n elementSelector?: string\n capturedAt: string\n}\n\nconst SEVERITY_ORDER: Record<UiFinding['severity'], number> = {\n critical: 0,\n high: 1,\n med: 2,\n low: 3,\n}\n\n// Validate workspaceDir at every public entry point. The MCP tool checks\n// the same shape at the wire boundary, but the writer is independently\n// exported via `@tangle-network/agent-runtime/audit`, so direct callers\n// would otherwise bypass that defense. Absolute + no `..` segments matches\n// the MCP tool's contract — any path that the writer joins for I/O must be\n// rooted at a deterministic location chosen by the caller.\nfunction assertValidWorkspaceDir(dir: string): void {\n if (typeof dir !== 'string' || dir.length === 0) {\n throw new Error(\n `audit-writer: workspaceDir must be a non-empty string (got ${JSON.stringify(dir)})`,\n )\n }\n if (!path.isAbsolute(dir)) {\n throw new Error(\n `audit-writer: workspaceDir must be an absolute path (got ${JSON.stringify(dir)})`,\n )\n }\n if (dir.split(path.sep).includes('..')) {\n throw new Error(\n `audit-writer: workspaceDir must not contain '..' segments (got ${JSON.stringify(dir)})`,\n )\n }\n}\n\n/** @experimental */\nexport async function initAuditWorkspace(workspaceDir: string): Promise<void> {\n assertValidWorkspaceDir(workspaceDir)\n await fs.mkdir(path.join(workspaceDir, 'issues'), { recursive: true })\n await fs.mkdir(path.join(workspaceDir, 'screenshots'), { recursive: true })\n const regPath = path.join(workspaceDir, 'registry.json')\n try {\n await fs.access(regPath)\n } catch {\n const fresh: AuditRegistry = { schemaVersion: 1, findings: [], routes: {} }\n await fs.writeFile(regPath, JSON.stringify(fresh, null, 2))\n }\n}\n\n/** @experimental */\nexport async function readAuditRegistry(workspaceDir: string): Promise<AuditRegistry> {\n assertValidWorkspaceDir(workspaceDir)\n const regPath = path.join(workspaceDir, 'registry.json')\n const text = await fs.readFile(regPath, 'utf8')\n const parsed = JSON.parse(text) as unknown\n if (!parsed || typeof parsed !== 'object') {\n throw new Error(`audit-writer: registry.json at ${regPath} is not a JSON object`)\n }\n const reg = parsed as Partial<AuditRegistry>\n if (reg.schemaVersion !== 1 || !Array.isArray(reg.findings) || typeof reg.routes !== 'object') {\n throw new Error(`audit-writer: registry.json at ${regPath} has an unrecognized shape`)\n }\n return reg as AuditRegistry\n}\n\nasync function writeAuditRegistry(workspaceDir: string, reg: AuditRegistry): Promise<void> {\n const regPath = path.join(workspaceDir, 'registry.json')\n // Write to a sibling temp file and rename. POSIX rename is atomic, so a\n // reader at any instant sees either the pre-write OR post-write registry,\n // never a half-written file. Prevents corruption on crash mid-write.\n const tmpPath = `${regPath}.tmp-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`\n await fs.writeFile(tmpPath, JSON.stringify(reg, null, 2))\n await fs.rename(tmpPath, regPath)\n}\n\n// Per-workspace serialization. The writer reads registry.json, mutates it\n// in memory, then writes it back; concurrent calls would race on the\n// monotonic-id assignment and clobber each other's findings. Each\n// workspace key holds the Promise of the most-recently-enqueued operation;\n// new operations chain off it so they run strictly serially.\nconst workspaceLocks = new Map<string, Promise<unknown>>()\n\nasync function withWorkspaceLock<T>(workspaceDir: string, fn: () => Promise<T>): Promise<T> {\n const key = path.resolve(workspaceDir)\n const prev = workspaceLocks.get(key) ?? Promise.resolve()\n let signalDone!: () => void\n const done = new Promise<void>((resolve) => {\n signalDone = resolve\n })\n workspaceLocks.set(key, done)\n try {\n await prev\n return await fn()\n } finally {\n signalDone()\n // Only delete if no later caller has overwritten the slot — preserves\n // the chain for any concurrent callers still waiting.\n if (workspaceLocks.get(key) === done) workspaceLocks.delete(key)\n }\n}\n\nfunction assertFindingShape(f: UiFinding, index: number): void {\n const where = `audit-writer: findings[${index}]`\n if (!UI_LENSES.includes(f.lens)) {\n throw new Error(\n `${where}: invalid lens ${JSON.stringify(f.lens)}; one of ${UI_LENSES.join('|')}`,\n )\n }\n if (!UI_FINDING_SEVERITIES.includes(f.severity)) {\n throw new Error(\n `${where}: invalid severity ${JSON.stringify(f.severity)}; one of ${UI_FINDING_SEVERITIES.join('|')}`,\n )\n }\n for (const field of ['title', 'route', 'observation', 'impact', 'suggestedFix'] as const) {\n const v = f[field]\n if (typeof v !== 'string' || v.trim().length === 0) {\n throw new Error(`${where}: ${field} must be a non-empty string`)\n }\n }\n if (!Array.isArray(f.screenshots) || f.screenshots.length === 0) {\n throw new Error(`${where}: screenshots must be a non-empty array`)\n }\n for (let i = 0; i < f.screenshots.length; i += 1) {\n const s = f.screenshots[i]\n if (!s || typeof s.path !== 'string' || s.path.length === 0) {\n throw new Error(`${where}.screenshots[${i}].path must be a non-empty string`)\n }\n // Defense-in-depth: screenshot paths are written verbatim into Markdown\n // and the registry; a downstream consumer that joins workspaceDir with\n // them would otherwise be exposed to traversal-read. Reject `..`\n // segments at the validation boundary.\n if (s.path.split(/[/\\\\]/).includes('..')) {\n throw new Error(`${where}.screenshots[${i}].path must not contain '..' segments`)\n }\n }\n}\n\nfunction nextFindingId(reg: AuditRegistry): number {\n let max = 0\n for (const f of reg.findings) {\n if (typeof f.id === 'number' && f.id > max) max = f.id\n }\n return max + 1\n}\n\nfunction slugifyTitle(title: string): string {\n return slugify(title, 'title')\n}\n\nfunction renderFinding(finding: UiFinding): string {\n if (!finding.id) {\n throw new Error('audit-writer: cannot render a finding without an assigned id')\n }\n if (finding.screenshots.length === 0) {\n throw new Error(`audit-writer: finding #${finding.id} has no screenshots`)\n }\n const id = finding.id\n const numStr = String(id).padStart(3, '0')\n\n const metaLines: string[] = [\n `> **Issue #${numStr}** · **Severity:** ${finding.severity} · **Lens:** ${finding.lens}`,\n `> **Route:** \\`${finding.route}\\`${finding.url ? ` · **URL:** ${finding.url}` : ''}`,\n ]\n if (finding.viewport) metaLines.push(`> **Viewport:** ${finding.viewport}`)\n if (finding.selector) metaLines.push(`> **Selector:** \\`${finding.selector}\\``)\n if (finding.tags && finding.tags.length > 0) {\n metaLines.push(`> **Tags:** ${finding.tags.map((t) => `\\`${t}\\``).join(', ')}`)\n }\n if (finding.similarTo && finding.similarTo.length > 0) {\n metaLines.push(\n `> **Possible duplicates:** ${finding.similarTo.map((n) => `#${String(n).padStart(3, '0')}`).join(', ')}`,\n )\n }\n metaLines.push(`> _Generated: ${finding.createdAt ?? new Date().toISOString()}_`)\n\n const reproSteps =\n finding.reproSteps ??\n `1. Open \\`${finding.route}\\`${finding.url ? ` (${finding.url})` : ''}\\n2. Observe the area shown in the screenshot${finding.selector ? ` (selector: \\`${finding.selector}\\`)` : ''}.`\n\n // Screenshot paths are workspace-relative; the issue file sits in\n // `<workspace>/issues/`, so we render `../<path>`.\n const evidence = finding.screenshots\n .map((s) => ``)\n .join('\\n\\n')\n\n return [\n `# [UI] ${finding.title}`,\n '',\n metaLines.join('\\n'),\n '',\n '## Observation',\n finding.observation.trim(),\n '',\n '## Why it matters',\n finding.impact.trim(),\n '',\n '## Steps to reproduce',\n reproSteps,\n '',\n '## Suggested fix',\n finding.suggestedFix.trim(),\n '',\n '## Evidence',\n evidence,\n '',\n '---',\n `<sub>Generated by agent-runtime/ui-auditor · lens=\\`${finding.lens}\\` · severity=\\`${finding.severity}\\`</sub>`,\n '',\n ].join('\\n')\n}\n\n/** @experimental */\nexport interface AppendFindingsResult {\n /** Findings with id + createdAt assigned, in input order. */\n written: UiFinding[]\n /** Workspace-relative path to each issue Markdown file, in input order. */\n files: string[]\n}\n\n/**\n * Append findings to a workspace, writing one Markdown file per finding\n * and updating registry.json. Assigns monotonically increasing ids to\n * findings that arrived without one.\n *\n * Findings already carrying an id that collides with the registry are\n * rejected — callers must either freshly mint findings (id undefined) or\n * use a separate update path. This protects against accidental overwrite.\n *\n * @experimental\n */\nexport async function appendFindings(\n workspaceDir: string,\n findings: readonly UiFinding[],\n): Promise<AppendFindingsResult> {\n assertValidWorkspaceDir(workspaceDir)\n // Validate every finding BEFORE acquiring the lock so callers see a fast\n // input-shape error without blocking concurrent writers. Validation is\n // the only defense against path traversal via crafted `lens` values\n // (the filename interpolates lens directly) and against malformed\n // Markdown from missing required fields — TypeScript types are erased\n // at runtime, so this boundary cannot trust its callers.\n for (let i = 0; i < findings.length; i += 1) {\n const f = findings[i]\n if (!f) throw new Error(`audit-writer: findings[${i}] is undefined`)\n assertFindingShape(f, i)\n }\n\n return withWorkspaceLock(workspaceDir, async () => {\n await initAuditWorkspace(workspaceDir)\n const reg = await readAuditRegistry(workspaceDir)\n const usedIds = new Set<number>()\n for (const f of reg.findings) {\n if (typeof f.id === 'number') usedIds.add(f.id)\n }\n\n const written: UiFinding[] = []\n const files: string[] = []\n let nextId = nextFindingId(reg)\n\n for (const incoming of findings) {\n let id = incoming.id\n if (id !== undefined) {\n if (usedIds.has(id)) {\n throw new Error(\n `audit-writer: incoming finding id ${id} (title=${JSON.stringify(incoming.title)}) collides with an existing registry entry`,\n )\n }\n } else {\n id = nextId\n while (usedIds.has(id)) id += 1\n nextId = id + 1\n }\n usedIds.add(id)\n\n const createdAt = incoming.createdAt ?? new Date().toISOString()\n const persisted: UiFinding = { ...incoming, id, createdAt }\n\n const slug = slugifyTitle(persisted.title)\n const fileName = `${String(id).padStart(3, '0')}--${persisted.lens}--${slug}.md`\n const filePathAbs = path.join(workspaceDir, 'issues', fileName)\n const filePathRel = `issues/${fileName}`\n\n await fs.writeFile(filePathAbs, renderFinding(persisted))\n reg.findings.push(persisted)\n written.push(persisted)\n files.push(filePathRel)\n }\n\n await writeAuditRegistry(workspaceDir, reg)\n return { written, files }\n })\n}\n\n/** @experimental */\nexport interface RegisterCapturesOptions {\n route: string\n url?: string\n captures: readonly AuditRegistryCapture[]\n}\n\n/**\n * Record screenshots taken for a route in the registry, without filing a\n * finding. Useful when the auditor wants to remember which captures\n * exist for resume / dedup purposes.\n *\n * @experimental\n */\nexport async function registerCaptures(\n workspaceDir: string,\n options: RegisterCapturesOptions,\n): Promise<void> {\n assertValidWorkspaceDir(workspaceDir)\n if (typeof options.route !== 'string' || options.route.trim().length === 0) {\n throw new Error('audit-writer: registerCaptures: route must be a non-empty string')\n }\n return withWorkspaceLock(workspaceDir, async () => {\n await initAuditWorkspace(workspaceDir)\n const reg = await readAuditRegistry(workspaceDir)\n const slot = reg.routes[options.route] ?? { captures: [] }\n if (options.url) slot.url = options.url\n slot.captures = [...slot.captures, ...options.captures]\n reg.routes[options.route] = slot\n await writeAuditRegistry(workspaceDir, reg)\n })\n}\n\n/** @experimental */\nexport interface AuditIndex {\n /** Total findings in the workspace. */\n total: number\n bySeverity: Record<UiFinding['severity'], number>\n byLens: Partial<Record<UiLens, number>>\n byRoute: Record<string, number>\n}\n\n/** @experimental */\nexport function summarizeRegistry(reg: AuditRegistry): AuditIndex {\n const bySeverity: Record<UiFinding['severity'], number> = {\n critical: 0,\n high: 0,\n med: 0,\n low: 0,\n }\n const byLens: Partial<Record<UiLens, number>> = {}\n const byRoute: Record<string, number> = {}\n for (const f of reg.findings) {\n bySeverity[f.severity] += 1\n byLens[f.lens] = (byLens[f.lens] ?? 0) + 1\n byRoute[f.route] = (byRoute[f.route] ?? 0) + 1\n }\n return { total: reg.findings.length, bySeverity, byLens, byRoute }\n}\n\n/**\n * Regenerate `<workspace>/index.md` from registry.json.\n *\n * @experimental\n */\nexport async function writeAuditIndex(workspaceDir: string): Promise<string> {\n assertValidWorkspaceDir(workspaceDir)\n const reg = await readAuditRegistry(workspaceDir)\n const summary = summarizeRegistry(reg)\n\n const sorted = [...reg.findings].sort((a, b) => {\n const sa = SEVERITY_ORDER[a.severity]\n const sb = SEVERITY_ORDER[b.severity]\n if (sa !== sb) return sa - sb\n return (a.id ?? 0) - (b.id ?? 0)\n })\n\n const lines: string[] = []\n lines.push('# UI audit index', '')\n lines.push(`_Generated: ${new Date().toISOString()}_`, '')\n lines.push(`**Total findings:** ${summary.total}`, '')\n lines.push('## By severity')\n for (const s of ['critical', 'high', 'med', 'low'] as const) {\n const n = summary.bySeverity[s] ?? 0\n if (n > 0) lines.push(`- **${s}** — ${n}`)\n }\n lines.push('', '## By lens')\n const lensEntries: [string, number][] = Object.entries(summary.byLens).map(([k, v]) => [\n k,\n v ?? 0,\n ])\n for (const [lens, n] of lensEntries.sort((a, b) => b[1] - a[1])) {\n lines.push(`- **${lens}** — ${n}`)\n }\n lines.push('', '## By route')\n const routeEntries: [string, number][] = Object.entries(summary.byRoute).map(([k, v]) => [\n k,\n v ?? 0,\n ])\n for (const [route, n] of routeEntries.sort((a, b) => b[1] - a[1])) {\n lines.push(`- \\`${route}\\` — ${n}`)\n }\n lines.push('', '## Findings', '')\n for (const f of sorted) {\n if (typeof f.id !== 'number') continue\n const num = String(f.id).padStart(3, '0')\n const slug = slugifyTitle(f.title)\n const file = `issues/${num}--${f.lens}--${slug}.md`\n lines.push(`- [#${num} \\`${f.severity}\\` \\`${f.lens}\\` \\`${f.route}\\` — ${f.title}](${file})`)\n }\n lines.push('')\n\n const out = path.join(workspaceDir, 'index.md')\n const body = lines.join('\\n')\n await fs.writeFile(out, body)\n return out\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/profiles/ui-auditor/slugify.ts
|
|
2
|
+
function slugify(value, fieldName) {
|
|
3
|
+
const slug = value.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
4
|
+
if (slug.length === 0) {
|
|
5
|
+
throw new Error(
|
|
6
|
+
`ui-auditor: ${fieldName} slugified to empty string. Provide a non-empty value (got ${JSON.stringify(value)}).`
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
return slug;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
slugify
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=chunk-4B6U4CVQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/profiles/ui-auditor/slugify.ts"],"sourcesContent":["/**\n * @experimental\n *\n * Shared slug helper used by both the in-process auditor client (for\n * screenshot filenames) and the issue writer (for issue Markdown\n * filenames). Lowercases, normalizes, strips non-alphanumeric, trims\n * dashes, caps at 80 chars. Throws when the result is empty so callers\n * never silently write to a name-less filename.\n */\n\n/** @experimental */\nexport function slugify(value: string, fieldName: string): string {\n const slug = value\n .toLowerCase()\n .normalize('NFKD')\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 80)\n if (slug.length === 0) {\n throw new Error(\n `ui-auditor: ${fieldName} slugified to empty string. Provide a non-empty value (got ${JSON.stringify(value)}).`,\n )\n }\n return slug\n}\n"],"mappings":";AAWO,SAAS,QAAQ,OAAe,WAA2B;AAChE,QAAM,OAAO,MACV,YAAY,EACZ,UAAU,MAAM,EAChB,QAAQ,eAAe,GAAG,EAC1B,QAAQ,YAAY,EAAE,EACtB,MAAM,GAAG,EAAE;AACd,MAAI,KAAK,WAAW,GAAG;AACrB,UAAM,IAAI;AAAA,MACR,eAAe,SAAS,8DAA8D,KAAK,UAAU,KAAK,CAAC;AAAA,IAC7G;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
|