mason-context 0.16.2 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +14 -10
- package/dist/mason-audit.js +1719 -751
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-auto.js +1963 -1244
- package/dist/mason-auto.js.map +1 -1
- package/dist/mason-drift.js +13 -0
- package/dist/mason-drift.js.map +1 -1
- package/dist/mason-hook.js +13 -0
- package/dist/mason-hook.js.map +1 -1
- package/dist/mason-mcp.js +2777 -1934
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason-review.js +127 -53
- package/dist/mason-review.js.map +1 -1
- package/dist/mason.js +2115 -1396
- package/dist/mason.js.map +1 -1
- package/package.json +5 -1
package/dist/mason-auto.js
CHANGED
|
@@ -248,391 +248,255 @@ var init_storage = __esm({
|
|
|
248
248
|
}
|
|
249
249
|
});
|
|
250
250
|
|
|
251
|
-
// src/automation/
|
|
251
|
+
// src/automation/store.ts
|
|
252
|
+
import fs3 from "fs/promises";
|
|
252
253
|
import os from "os";
|
|
253
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
254
254
|
import { z } from "zod";
|
|
255
|
-
function
|
|
256
|
-
try {
|
|
257
|
-
return executionSchema.parse(raw);
|
|
258
|
-
} catch (error) {
|
|
259
|
-
throw new Error("Invalid automation execution store; receipt history was retained.", { cause: error });
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
function automationFailure(error) {
|
|
263
|
-
const recorded = failureSchema.safeParse(error?.failure);
|
|
264
|
-
if (recorded.success) return recorded.data;
|
|
265
|
-
const message2 = (error instanceof Error ? error.message : String(error)).replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 700);
|
|
266
|
-
const codes = /* @__PURE__ */ new Set();
|
|
267
|
-
let cause = error;
|
|
268
|
-
for (let i = 0; cause && i < 8; i++) {
|
|
269
|
-
codes.add(String(cause.code));
|
|
270
|
-
cause = cause.cause;
|
|
271
|
-
}
|
|
272
|
-
const code = codes.has("ENOSPC") || codes.has("EDQUOT") ? "storage-full" : /changed during|changed while|changed between/.test(message2) ? "inputs-changed" : /Automation is busy/.test(message2) ? "busy" : /not a git repository|unknown revision|bad revision|different history|unreachable/.test(message2) ? "history-unavailable" : error instanceof z.ZodError || error instanceof SyntaxError || /Hook input|Expected one command|Unknown automation command|--host/.test(message2) ? "invalid-input" : /store|baseline|modified|symbolic link|Symlink|automation state|state belongs/.test(message2) ? "invalid-evidence" : [...codes].some((c) => /^E[A-Z]+$/.test(c)) ? "io-error" : "internal";
|
|
273
|
-
return { code, message: message2, retryable: ["inputs-changed", "storage-full", "busy", "io-error"].includes(code), receiptRecorded: false };
|
|
274
|
-
}
|
|
275
|
-
function failureMessage(error) {
|
|
276
|
-
const failure = automationFailure(error);
|
|
277
|
-
return `Mason automation unavailable [${failure.code}]; evidence capture/verification was not established. ${failure.message}` + (failure.receiptRecorded ? "" : " No durable failure receipt was recorded.");
|
|
278
|
-
}
|
|
279
|
-
async function recordExecution(root, directory, event, run) {
|
|
280
|
-
const file = directory + "/execution.json";
|
|
281
|
-
const raw = await readStoreJson(root, file);
|
|
282
|
-
const log = raw === null ? { version: 1, attempts: [], discardedAttempts: 0 } : parseExecution(raw);
|
|
283
|
-
for (const attempt2 of log.attempts) if (attempt2.status === "running") attempt2.status = "unknown";
|
|
284
|
-
log.discardedAttempts += Math.max(0, log.attempts.length - 31);
|
|
285
|
-
log.attempts = log.attempts.slice(-31);
|
|
286
|
-
const started = performance.now();
|
|
287
|
-
const attempt = {
|
|
288
|
-
id: randomUUID2(),
|
|
289
|
-
event,
|
|
290
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
291
|
-
pid: process.pid,
|
|
292
|
-
host: os.hostname(),
|
|
293
|
-
status: "running"
|
|
294
|
-
};
|
|
295
|
-
log.attempts.push(attempt);
|
|
296
|
-
await writeStoreJson(root, file, log);
|
|
255
|
+
function parseState(raw) {
|
|
297
256
|
try {
|
|
298
|
-
|
|
299
|
-
Object.assign(attempt, {
|
|
300
|
-
status: "completed",
|
|
301
|
-
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
302
|
-
durationMs: performance.now() - started,
|
|
303
|
-
verificationStatus: result.report.status,
|
|
304
|
-
reportPath: result.report.reportPath
|
|
305
|
-
});
|
|
306
|
-
await writeStoreJson(root, file, log);
|
|
307
|
-
return result;
|
|
257
|
+
return stateSchema.parse(raw);
|
|
308
258
|
} catch (error) {
|
|
309
|
-
|
|
310
|
-
Object.assign(attempt, { status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), durationMs: performance.now() - started, failure });
|
|
311
|
-
delete attempt.verificationStatus;
|
|
312
|
-
delete attempt.reportPath;
|
|
313
|
-
try {
|
|
314
|
-
await writeStoreJson(root, file, { ...log, attempts: log.attempts.map((a) => a === attempt ? { ...a, failure: { ...failure, receiptRecorded: true } } : a) });
|
|
315
|
-
failure.receiptRecorded = true;
|
|
316
|
-
} catch {
|
|
317
|
-
}
|
|
318
|
-
throw Object.assign(new Error(failure.message, { cause: error }), { failure });
|
|
259
|
+
throw new Error("Invalid automation state; original evidence was retained.", { cause: error });
|
|
319
260
|
}
|
|
320
261
|
}
|
|
321
|
-
async function
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (log.attempts.some((attempt) => attempt.status === "running")) {
|
|
262
|
+
async function withLock(root, directory, run, waitMs = 5e3) {
|
|
263
|
+
const file = await storePath(root, directory + "/lock", true);
|
|
264
|
+
const deadline = Date.now() + waitMs;
|
|
265
|
+
let handle;
|
|
266
|
+
while (!handle) {
|
|
327
267
|
try {
|
|
328
|
-
|
|
329
|
-
} catch {
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
for (const attempt of log.attempts) {
|
|
333
|
-
if (attempt.status !== "running") continue;
|
|
334
|
-
let alive = false;
|
|
335
|
-
if (attempt.host === os.hostname() && lock?.pid === attempt.pid && lock.host === attempt.host) {
|
|
268
|
+
handle = await fs3.open(file, "wx", 384);
|
|
336
269
|
try {
|
|
337
|
-
|
|
338
|
-
alive = true;
|
|
270
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
|
|
339
271
|
} catch (error) {
|
|
340
|
-
|
|
272
|
+
await handle.close().catch(() => {
|
|
273
|
+
});
|
|
274
|
+
handle = void 0;
|
|
275
|
+
await fs3.rm(file, { force: true }).catch(() => {
|
|
276
|
+
});
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
} catch (error) {
|
|
280
|
+
if (error.code !== "EEXIST") throw error;
|
|
281
|
+
try {
|
|
282
|
+
const owner = JSON.parse(await fs3.readFile(file, "utf8"));
|
|
283
|
+
if (owner.host === os.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
|
|
284
|
+
try {
|
|
285
|
+
process.kill(owner.pid, 0);
|
|
286
|
+
} catch (probe) {
|
|
287
|
+
if (probe.code === "ESRCH") {
|
|
288
|
+
const reclaim = file + ".reclaim";
|
|
289
|
+
let guard;
|
|
290
|
+
try {
|
|
291
|
+
guard = await fs3.open(reclaim, "wx", 384);
|
|
292
|
+
const current = JSON.parse(await fs3.readFile(file, "utf8"));
|
|
293
|
+
if (current.pid === owner.pid && current.host === owner.host) await fs3.unlink(file);
|
|
294
|
+
} finally {
|
|
295
|
+
if (guard) {
|
|
296
|
+
await guard.close();
|
|
297
|
+
await fs3.rm(reclaim, { force: true });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
} catch {
|
|
341
304
|
}
|
|
305
|
+
if (Date.now() >= deadline) throw new Error("Automation is busy or its lock needs inspection: " + file);
|
|
306
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
342
307
|
}
|
|
343
|
-
if (!alive) attempt.status = "unknown";
|
|
344
308
|
}
|
|
345
|
-
|
|
309
|
+
try {
|
|
310
|
+
return await run();
|
|
311
|
+
} finally {
|
|
312
|
+
await handle.close();
|
|
313
|
+
await fs3.unlink(file);
|
|
314
|
+
}
|
|
346
315
|
}
|
|
347
|
-
var
|
|
348
|
-
var
|
|
349
|
-
"src/automation/
|
|
316
|
+
var hostSchema, events, stateSchema;
|
|
317
|
+
var init_store = __esm({
|
|
318
|
+
"src/automation/store.ts"() {
|
|
350
319
|
"use strict";
|
|
351
320
|
init_storage();
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
321
|
+
hostSchema = z.enum(["claude", "codex"]);
|
|
322
|
+
events = ["session_start", "turn_start", "before_tool", "after_tool", "task_end"];
|
|
323
|
+
stateSchema = z.object({
|
|
324
|
+
version: z.literal(1),
|
|
325
|
+
root: z.string(),
|
|
326
|
+
gitDir: z.string(),
|
|
327
|
+
branch: z.string(),
|
|
328
|
+
baselines: z.array(z.object({ path: z.string(), at: z.string(), event: z.string(), fingerprint: z.string() })).max(128),
|
|
329
|
+
sessions: z.record(z.object({
|
|
330
|
+
host: hostSchema,
|
|
331
|
+
seen: z.string().nullable(),
|
|
332
|
+
continued: z.boolean(),
|
|
333
|
+
initialIssues: z.array(z.string()),
|
|
334
|
+
initialDocs: z.record(z.string().nullable()),
|
|
335
|
+
lastUsed: z.string(),
|
|
336
|
+
mutationObserved: z.boolean(),
|
|
337
|
+
pending: z.record(z.string()),
|
|
338
|
+
coverageGaps: z.array(z.string()),
|
|
339
|
+
events: z.record(z.object({ at: z.string(), count: z.number().int().positive() }))
|
|
340
|
+
})),
|
|
341
|
+
updatedAt: z.string(),
|
|
342
|
+
fingerprint: z.string().nullable(),
|
|
343
|
+
latest: z.string().nullable(),
|
|
344
|
+
analysis: z.object({ id: z.string(), fingerprint: z.string(), completedAt: z.number(), path: z.string() }).optional()
|
|
370
345
|
});
|
|
371
|
-
executionSchema = z.object({ version: z.literal(1), attempts: z.array(attemptSchema).max(32), discardedAttempts: z.number().int().nonnegative().default(0) });
|
|
372
346
|
}
|
|
373
347
|
});
|
|
374
348
|
|
|
375
|
-
// src/
|
|
349
|
+
// src/audit/commands.ts
|
|
376
350
|
import path4 from "path";
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
"
|
|
383
|
-
|
|
384
|
-
"**/*Test.java",
|
|
385
|
-
"**/*Tests.kt",
|
|
386
|
-
"**/*Tests.java",
|
|
387
|
-
"**/test_*.py",
|
|
388
|
-
"**/*_test.py",
|
|
389
|
-
"**/*_test.go",
|
|
390
|
-
"**/*Tests.swift",
|
|
391
|
-
"**/*Test.swift",
|
|
392
|
-
"**/*_test.rs"
|
|
393
|
-
];
|
|
394
|
-
const testFiles = await access.list(testPatterns);
|
|
395
|
-
const sourceFiles = await access.list();
|
|
396
|
-
const sourceByBaseName = /* @__PURE__ */ new Map();
|
|
397
|
-
for (const file of sourceFiles) {
|
|
398
|
-
if (testFiles.includes(file)) continue;
|
|
399
|
-
const baseName = path4.basename(file).replace(/\.[^.]+$/, "");
|
|
400
|
-
const existing = sourceByBaseName.get(baseName) ?? [];
|
|
401
|
-
existing.push(file);
|
|
402
|
-
sourceByBaseName.set(baseName, existing);
|
|
403
|
-
}
|
|
404
|
-
const paired = [];
|
|
405
|
-
const unmatched = [];
|
|
406
|
-
for (const testFile of testFiles) {
|
|
407
|
-
const testBaseName = path4.basename(testFile).replace(/\.[^.]+$/, "");
|
|
408
|
-
const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
|
|
409
|
-
if (!sourceName) {
|
|
410
|
-
unmatched.push(testFile);
|
|
351
|
+
function splitCommands(line) {
|
|
352
|
+
const parts = [];
|
|
353
|
+
let quote = "", start = 0;
|
|
354
|
+
for (let i = 0; i < line.length; i++) {
|
|
355
|
+
const char = line[i];
|
|
356
|
+
if (char === "\\") {
|
|
357
|
+
i++;
|
|
411
358
|
continue;
|
|
412
359
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
source: bestMatch,
|
|
426
|
-
confidence: candidates.length === 1 ? "exact" : "best-guess"
|
|
427
|
-
});
|
|
428
|
-
} else {
|
|
429
|
-
unmatched.push(testFile);
|
|
360
|
+
if (quote) {
|
|
361
|
+
if (char === quote) quote = "";
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (char === "'" || char === '"') {
|
|
365
|
+
quote = char;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (char === ";" || line.slice(i, i + 2) === "&&") {
|
|
369
|
+
parts.push(line.slice(start, i));
|
|
370
|
+
if (char === "&") i++;
|
|
371
|
+
start = i + 1;
|
|
430
372
|
}
|
|
431
373
|
}
|
|
432
|
-
|
|
374
|
+
parts.push(line.slice(start));
|
|
375
|
+
return parts;
|
|
433
376
|
}
|
|
434
|
-
function
|
|
435
|
-
const
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if (
|
|
440
|
-
|
|
377
|
+
function commandClaims(line, lineNo, cwd, shell = false) {
|
|
378
|
+
const claims = [];
|
|
379
|
+
const segments = splitCommands(line);
|
|
380
|
+
for (const segment of segments) {
|
|
381
|
+
const text2 = segment.trim().replace(/^\$\s+/, "");
|
|
382
|
+
if (text2.startsWith("#") || /^(?:echo|printf)\b/.test(text2)) continue;
|
|
383
|
+
const cd = text2.match(/^cd\s+(.+)$/);
|
|
384
|
+
if (cd) {
|
|
385
|
+
const directory = literal(cd[1]);
|
|
386
|
+
cwd = directory === null || cwd === null || /\s/.test(directory) && !/^["']/.test(cd[1]) ? null : path4.posix.join(cwd ?? ".", directory);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
for (const match of text2.matchAll(COMMAND)) {
|
|
390
|
+
const flags = match[2].trim();
|
|
391
|
+
const selector = flags.match(/^(?:--prefix|--dir|-C)(?:\s+|=)(.+)$/);
|
|
392
|
+
let directory = cwd;
|
|
393
|
+
let scopeUnknown;
|
|
394
|
+
if (shell && match.index !== 0 || match[3].startsWith("-")) scopeUnknown = "Unsupported shell wrapper or options before the script.";
|
|
395
|
+
if (selector) {
|
|
396
|
+
const selected = literal(selector[1]);
|
|
397
|
+
directory = selected === null || cwd === null ? null : path4.posix.join(cwd ?? ".", selected);
|
|
398
|
+
} else if (flags) scopeUnknown = "Unsupported package-manager options before run: " + flags;
|
|
399
|
+
const tail = text2.slice(match.index + match[0].length).split(/\s--(?:\s|$)/)[0];
|
|
400
|
+
if (/(?:^|\s)(?:--workspace(?:s)?|-w|--filter|-F|--prefix|--dir|-C|--if-present)(?:\s|=|$)/.test(tail)) {
|
|
401
|
+
scopeUnknown = "Package selection or conditional options after the script need review.";
|
|
402
|
+
}
|
|
403
|
+
if (/[|()]/.test(text2) || /\b(?:pushd|popd|if|for|while)\b/.test(text2) || directory === null) {
|
|
404
|
+
scopeUnknown = "The command's working directory or shell control flow could not be resolved.";
|
|
405
|
+
}
|
|
406
|
+
claims.push({
|
|
407
|
+
scriptName: match[3],
|
|
408
|
+
invocation: match[0],
|
|
409
|
+
excerpt: match[0],
|
|
410
|
+
line: lineNo,
|
|
411
|
+
...directory !== void 0 && directory !== null ? { directory } : {},
|
|
412
|
+
...scopeUnknown ? { scopeUnknown } : {}
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
if (/\b(?:cd|pushd|popd)\b/.test(text2)) cwd = null;
|
|
441
416
|
}
|
|
442
|
-
return
|
|
417
|
+
return { claims, cwd };
|
|
443
418
|
}
|
|
444
|
-
var
|
|
445
|
-
|
|
419
|
+
var TOKEN, COMMAND, literal;
|
|
420
|
+
var init_commands = __esm({
|
|
421
|
+
"src/audit/commands.ts"() {
|
|
446
422
|
"use strict";
|
|
447
|
-
|
|
423
|
+
TOKEN = `(?:"[^"\\n]*"|'[^'\\n]*'|[^\\s\`;&|]+)`;
|
|
424
|
+
COMMAND = new RegExp(`\\b(npm|pnpm|yarn)((?:[ \\t]+${TOKEN})*?)[ \\t]+run[ \\t]+([A-Za-z0-9:_.-]+)`, "g");
|
|
425
|
+
literal = (value) => {
|
|
426
|
+
const unquoted = value.replace(/^(["'])(.*)\1$/, "$2");
|
|
427
|
+
return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) || path4.posix.isAbsolute(unquoted) ? null : unquoted;
|
|
428
|
+
};
|
|
448
429
|
}
|
|
449
430
|
});
|
|
450
431
|
|
|
451
|
-
// src/
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
async function inspectSnapshot(rootDir) {
|
|
457
|
-
try {
|
|
458
|
-
const raw = await readStoreJson(rootDir, ".mason/snapshot.json");
|
|
459
|
-
const snapshot = raw === null ? null : snapshotSchema.parse(raw);
|
|
460
|
-
return { status: snapshot ? "available" : "missing", snapshot, diagnostics: [] };
|
|
461
|
-
} catch (error) {
|
|
462
|
-
return { status: "invalid", snapshot: null, diagnostics: [{
|
|
463
|
-
path: ".mason/snapshot.json",
|
|
464
|
-
message: error instanceof Error ? error.message : String(error)
|
|
465
|
-
}] };
|
|
432
|
+
// src/audit/tree.ts
|
|
433
|
+
function glyphIndex(line) {
|
|
434
|
+
for (const glyph of GLYPHS) {
|
|
435
|
+
const idx = line.indexOf(glyph);
|
|
436
|
+
if (idx !== -1) return idx;
|
|
466
437
|
}
|
|
438
|
+
return -1;
|
|
467
439
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
440
|
+
function isSpacerLine(line) {
|
|
441
|
+
return /^[\s│|]*$/.test(line);
|
|
442
|
+
}
|
|
443
|
+
function entryName(afterGlyph) {
|
|
444
|
+
let name = afterGlyph.replace(/^\s+/, "");
|
|
445
|
+
const hash2 = name.search(/\s+#/);
|
|
446
|
+
if (hash2 !== -1) name = name.slice(0, hash2);
|
|
447
|
+
const columns = name.search(/\s{2,}/);
|
|
448
|
+
if (columns !== -1) name = name.slice(0, columns);
|
|
449
|
+
name = name.trim();
|
|
450
|
+
if (!name || /\s/.test(name)) return null;
|
|
451
|
+
return name;
|
|
452
|
+
}
|
|
453
|
+
function extractTreeClaims(blockLines, blockStartLine) {
|
|
454
|
+
const glyphLines = blockLines.filter((l) => glyphIndex(l) !== -1).length;
|
|
455
|
+
if (glyphLines < MIN_GLYPH_LINES) return [];
|
|
456
|
+
const claims = [];
|
|
457
|
+
const stack = [];
|
|
458
|
+
let rootPrefix = "";
|
|
459
|
+
let started = false;
|
|
460
|
+
for (let i = 0; i < blockLines.length; i++) {
|
|
461
|
+
const line = blockLines[i];
|
|
462
|
+
const col = glyphIndex(line);
|
|
463
|
+
if (col === -1) {
|
|
464
|
+
if (isSpacerLine(line)) continue;
|
|
465
|
+
if (!started) {
|
|
466
|
+
const candidate = line.trim();
|
|
467
|
+
if (candidate.endsWith("/") && !/\s/.test(candidate)) {
|
|
468
|
+
rootPrefix = candidate.replace(/\/+$/, "");
|
|
469
|
+
claims.push({
|
|
470
|
+
path: rootPrefix,
|
|
471
|
+
line: blockStartLine + i,
|
|
472
|
+
excerpt: candidate
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
return claims;
|
|
478
|
+
}
|
|
479
|
+
started = true;
|
|
480
|
+
const name = entryName(line.slice(col + GLYPHS[0].length));
|
|
481
|
+
if (name === null) return claims;
|
|
482
|
+
while (stack.length > 0 && stack[stack.length - 1].col >= col) {
|
|
483
|
+
stack.pop();
|
|
484
|
+
}
|
|
485
|
+
const isDir = name.endsWith("/");
|
|
486
|
+
const cleanName = name.replace(/\/+$/, "");
|
|
487
|
+
const segments = [
|
|
488
|
+
...rootPrefix ? [rootPrefix] : [],
|
|
489
|
+
...stack.map((s) => s.name),
|
|
490
|
+
cleanName
|
|
491
|
+
];
|
|
492
|
+
claims.push({
|
|
493
|
+
path: segments.join("/"),
|
|
494
|
+
line: blockStartLine + i,
|
|
495
|
+
excerpt: name
|
|
472
496
|
});
|
|
473
|
-
|
|
474
|
-
} catch {
|
|
475
|
-
return "unknown";
|
|
497
|
+
if (isDir) stack.push({ col, name: cleanName });
|
|
476
498
|
}
|
|
477
|
-
|
|
478
|
-
var exec2, repoPath, verificationFields, featureSchema, flowSchema, snapshotSchema;
|
|
479
|
-
var init_snapshot = __esm({
|
|
480
|
-
"src/snapshot/snapshot.ts"() {
|
|
481
|
-
"use strict";
|
|
482
|
-
init_files();
|
|
483
|
-
init_files();
|
|
484
|
-
init_storage();
|
|
485
|
-
init_paths();
|
|
486
|
-
init_test_map();
|
|
487
|
-
exec2 = promisify2(execFile2);
|
|
488
|
-
repoPath = z2.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
|
|
489
|
-
verificationFields = {
|
|
490
|
-
refreshedHash: z2.string().optional(),
|
|
491
|
-
verifiedAt: z2.string().optional(),
|
|
492
|
-
verifiedHash: z2.string().optional(),
|
|
493
|
-
verificationFailed: z2.boolean().optional(),
|
|
494
|
-
verificationNote: z2.string().optional()
|
|
495
|
-
};
|
|
496
|
-
featureSchema = z2.object({
|
|
497
|
-
description: z2.string(),
|
|
498
|
-
files: z2.array(repoPath),
|
|
499
|
-
tests: z2.array(repoPath).optional(),
|
|
500
|
-
type: z2.enum(["capability", "infrastructure"]).optional(),
|
|
501
|
-
...verificationFields
|
|
502
|
-
}).passthrough();
|
|
503
|
-
flowSchema = z2.object({ description: z2.string(), chain: z2.array(repoPath), ...verificationFields }).passthrough();
|
|
504
|
-
snapshotSchema = z2.object({
|
|
505
|
-
version: z2.literal(2),
|
|
506
|
-
createdAt: z2.string(),
|
|
507
|
-
updatedAt: z2.string(),
|
|
508
|
-
gitHash: z2.string(),
|
|
509
|
-
features: z2.record(featureSchema),
|
|
510
|
-
flows: z2.record(flowSchema)
|
|
511
|
-
}).passthrough();
|
|
512
|
-
}
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
// src/drift/drift.ts
|
|
516
|
-
import fs3 from "fs/promises";
|
|
517
|
-
import path6 from "path";
|
|
518
|
-
import { execFile as execFile3 } from "child_process";
|
|
519
|
-
import { promisify as promisify3 } from "util";
|
|
520
|
-
function parseChanges(output) {
|
|
521
|
-
const fields = output.split("\0");
|
|
522
|
-
const changes = [];
|
|
523
|
-
for (let i = 0; i < fields.length && fields[i]; ) {
|
|
524
|
-
const code = fields[i++];
|
|
525
|
-
const first = fields[i++];
|
|
526
|
-
if (!first) break;
|
|
527
|
-
const second = /^[RC]/.test(code) ? fields[i++] : void 0;
|
|
528
|
-
const change = second ? code.startsWith("R") ? { status: "renamed", path: second, previousPath: first } : { status: "added", path: second } : { status: code === "A" ? "added" : code === "D" ? "deleted" : "modified", path: first };
|
|
529
|
-
if (change.path.startsWith(".mason/") && (!change.previousPath || change.previousPath.startsWith(".mason/"))) continue;
|
|
530
|
-
changes.push(change);
|
|
531
|
-
}
|
|
532
|
-
return changes;
|
|
533
|
-
}
|
|
534
|
-
function touchedPaths(changes) {
|
|
535
|
-
return [...new Set(changes.flatMap((c) => c.previousPath ? [c.previousPath, c.path] : [c.path]))].sort();
|
|
536
|
-
}
|
|
537
|
-
async function getChangesWithStatus(resolvedRoot, fromHash, toHash = "HEAD") {
|
|
538
|
-
if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-") || !toHash || toHash === "unknown" || toHash.startsWith("-")) return null;
|
|
539
|
-
try {
|
|
540
|
-
const { stdout } = await exec3("git", ["diff", "--name-status", "-z", "-M", fromHash, toHash, "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 });
|
|
541
|
-
return parseChanges(stdout);
|
|
542
|
-
} catch {
|
|
543
|
-
return null;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
async function getWorkingTree(resolvedRoot) {
|
|
547
|
-
try {
|
|
548
|
-
const [diff, untracked] = await Promise.all([
|
|
549
|
-
exec3("git", ["diff", "--name-status", "-z", "-M", "HEAD", "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }),
|
|
550
|
-
exec3("git", ["ls-files", "-z", "--others", "--exclude-standard"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 })
|
|
551
|
-
]);
|
|
552
|
-
const untrackedFiles = untracked.stdout.split("\0").filter((f) => f && !f.startsWith(".mason/"));
|
|
553
|
-
return { available: true, changedFiles: [.../* @__PURE__ */ new Set([...touchedPaths(parseChanges(diff.stdout)), ...untrackedFiles])].sort(), untrackedFiles };
|
|
554
|
-
} catch {
|
|
555
|
-
return { available: false, changedFiles: [], untrackedFiles: [] };
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
var exec3;
|
|
559
|
-
var init_drift = __esm({
|
|
560
|
-
"src/drift/drift.ts"() {
|
|
561
|
-
"use strict";
|
|
562
|
-
init_snapshot();
|
|
563
|
-
init_paths();
|
|
564
|
-
exec3 = promisify3(execFile3);
|
|
565
|
-
}
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
// src/audit/tree.ts
|
|
569
|
-
function glyphIndex(line) {
|
|
570
|
-
for (const glyph of GLYPHS) {
|
|
571
|
-
const idx = line.indexOf(glyph);
|
|
572
|
-
if (idx !== -1) return idx;
|
|
573
|
-
}
|
|
574
|
-
return -1;
|
|
575
|
-
}
|
|
576
|
-
function isSpacerLine(line) {
|
|
577
|
-
return /^[\s│|]*$/.test(line);
|
|
578
|
-
}
|
|
579
|
-
function entryName(afterGlyph) {
|
|
580
|
-
let name = afterGlyph.replace(/^\s+/, "");
|
|
581
|
-
const hash2 = name.search(/\s+#/);
|
|
582
|
-
if (hash2 !== -1) name = name.slice(0, hash2);
|
|
583
|
-
const columns = name.search(/\s{2,}/);
|
|
584
|
-
if (columns !== -1) name = name.slice(0, columns);
|
|
585
|
-
name = name.trim();
|
|
586
|
-
if (!name || /\s/.test(name)) return null;
|
|
587
|
-
return name;
|
|
588
|
-
}
|
|
589
|
-
function extractTreeClaims(blockLines, blockStartLine) {
|
|
590
|
-
const glyphLines = blockLines.filter((l) => glyphIndex(l) !== -1).length;
|
|
591
|
-
if (glyphLines < MIN_GLYPH_LINES) return [];
|
|
592
|
-
const claims = [];
|
|
593
|
-
const stack = [];
|
|
594
|
-
let rootPrefix = "";
|
|
595
|
-
let started = false;
|
|
596
|
-
for (let i = 0; i < blockLines.length; i++) {
|
|
597
|
-
const line = blockLines[i];
|
|
598
|
-
const col = glyphIndex(line);
|
|
599
|
-
if (col === -1) {
|
|
600
|
-
if (isSpacerLine(line)) continue;
|
|
601
|
-
if (!started) {
|
|
602
|
-
const candidate = line.trim();
|
|
603
|
-
if (candidate.endsWith("/") && !/\s/.test(candidate)) {
|
|
604
|
-
rootPrefix = candidate.replace(/\/+$/, "");
|
|
605
|
-
claims.push({
|
|
606
|
-
path: rootPrefix,
|
|
607
|
-
line: blockStartLine + i,
|
|
608
|
-
excerpt: candidate
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
continue;
|
|
612
|
-
}
|
|
613
|
-
return claims;
|
|
614
|
-
}
|
|
615
|
-
started = true;
|
|
616
|
-
const name = entryName(line.slice(col + GLYPHS[0].length));
|
|
617
|
-
if (name === null) return claims;
|
|
618
|
-
while (stack.length > 0 && stack[stack.length - 1].col >= col) {
|
|
619
|
-
stack.pop();
|
|
620
|
-
}
|
|
621
|
-
const isDir = name.endsWith("/");
|
|
622
|
-
const cleanName = name.replace(/\/+$/, "");
|
|
623
|
-
const segments = [
|
|
624
|
-
...rootPrefix ? [rootPrefix] : [],
|
|
625
|
-
...stack.map((s) => s.name),
|
|
626
|
-
cleanName
|
|
627
|
-
];
|
|
628
|
-
claims.push({
|
|
629
|
-
path: segments.join("/"),
|
|
630
|
-
line: blockStartLine + i,
|
|
631
|
-
excerpt: name
|
|
632
|
-
});
|
|
633
|
-
if (isDir) stack.push({ col, name: cleanName });
|
|
634
|
-
}
|
|
635
|
-
return claims;
|
|
499
|
+
return claims;
|
|
636
500
|
}
|
|
637
501
|
var MIN_GLYPH_LINES, GLYPHS;
|
|
638
502
|
var init_tree = __esm({
|
|
@@ -644,6 +508,7 @@ var init_tree = __esm({
|
|
|
644
508
|
});
|
|
645
509
|
|
|
646
510
|
// src/audit/claims.ts
|
|
511
|
+
import MarkdownIt from "markdown-it";
|
|
647
512
|
function normalizePathToken(token) {
|
|
648
513
|
let t = token.trim();
|
|
649
514
|
if (!t) return null;
|
|
@@ -707,11 +572,34 @@ function extractClaims(content2) {
|
|
|
707
572
|
const counts = [];
|
|
708
573
|
const commands = /* @__PURE__ */ new Map();
|
|
709
574
|
const addPath = (claim) => {
|
|
710
|
-
|
|
575
|
+
const key = [claim.relativeTo ?? "scope", claim.path].join(":");
|
|
576
|
+
if (!paths.has(key)) paths.set(key, claim);
|
|
711
577
|
};
|
|
712
578
|
const addCommand = (claim) => {
|
|
713
|
-
|
|
579
|
+
const key = JSON.stringify([claim.scriptName, claim.directory, claim.scopeUnknown]);
|
|
580
|
+
if (!commands.has(key)) commands.set(key, claim);
|
|
714
581
|
};
|
|
582
|
+
const parsed = markdown.parse(lines.map((line, i) => ignored[i] ? "" : line).join("\n"), {});
|
|
583
|
+
for (const token of parsed) {
|
|
584
|
+
if (token.type !== "inline" || !token.map) continue;
|
|
585
|
+
let line = token.map[0] + 1;
|
|
586
|
+
for (const child of token.children ?? []) {
|
|
587
|
+
const target = child.type === "link_open" ? child.attrGet("href") : child.type === "image" ? child.attrGet("src") : null;
|
|
588
|
+
if (target && !/^(?:[a-z][a-z0-9+.-]*:|[\/#~])/i.test(target)) {
|
|
589
|
+
let pathname = null;
|
|
590
|
+
try {
|
|
591
|
+
pathname = decodeURIComponent(target.split(/[?#]/)[0]);
|
|
592
|
+
} catch {
|
|
593
|
+
}
|
|
594
|
+
if (pathname && !/[$*?{}<>\\\x00-\x1f]/.test(pathname)) {
|
|
595
|
+
addPath({ path: pathname, line, excerpt: target, relativeTo: "document" });
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (child.type === "softbreak" || child.type === "hardbreak") line++;
|
|
599
|
+
else line += child.content.split("\n").length - 1;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
let shellCwd;
|
|
715
603
|
let inFence = false;
|
|
716
604
|
let fenceInfo = "";
|
|
717
605
|
let fenceMarker = "";
|
|
@@ -742,6 +630,7 @@ function extractClaims(content2) {
|
|
|
742
630
|
fenceMarker = fenceMatch[1][0];
|
|
743
631
|
fenceInfo = fenceMatch[2].trim().toLowerCase();
|
|
744
632
|
blockLines = [];
|
|
633
|
+
shellCwd = void 0;
|
|
745
634
|
blockStartLine = lineNo + 1;
|
|
746
635
|
} else if (fenceMatch[1][0] === fenceMarker) {
|
|
747
636
|
inFence = false;
|
|
@@ -752,14 +641,9 @@ function extractClaims(content2) {
|
|
|
752
641
|
if (inFence) {
|
|
753
642
|
blockLines.push(ignored[i] ? "" : line);
|
|
754
643
|
if (!ignored[i] && SHELL_FENCE_INFOS.has(fenceInfo)) {
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
invocation: m[0],
|
|
759
|
-
line: lineNo,
|
|
760
|
-
excerpt: m[0]
|
|
761
|
-
});
|
|
762
|
-
}
|
|
644
|
+
const scanned = commandClaims(line, lineNo, shellCwd, true);
|
|
645
|
+
shellCwd = scanned.cwd;
|
|
646
|
+
for (const claim of scanned.claims) addCommand(claim);
|
|
763
647
|
}
|
|
764
648
|
continue;
|
|
765
649
|
}
|
|
@@ -786,14 +670,10 @@ function extractClaims(content2) {
|
|
|
786
670
|
excerpt: m[0]
|
|
787
671
|
});
|
|
788
672
|
}
|
|
789
|
-
for (const
|
|
790
|
-
addCommand(
|
|
791
|
-
scriptName: m[2],
|
|
792
|
-
invocation: m[0],
|
|
793
|
-
line: lineNo,
|
|
794
|
-
excerpt: m[0]
|
|
795
|
-
});
|
|
673
|
+
for (const match of line.matchAll(/`([^`]+)`/g)) {
|
|
674
|
+
for (const claim of commandClaims(match[1], lineNo, void 0, true).claims) addCommand(claim);
|
|
796
675
|
}
|
|
676
|
+
for (const claim of commandClaims(line.replace(/`[^`]+`/g, ""), lineNo).claims) addCommand(claim);
|
|
797
677
|
}
|
|
798
678
|
if (inFence) processBlock();
|
|
799
679
|
return {
|
|
@@ -802,10 +682,11 @@ function extractClaims(content2) {
|
|
|
802
682
|
commands: [...commands.values()]
|
|
803
683
|
};
|
|
804
684
|
}
|
|
805
|
-
var ROOT_FILE_NAMES, SHELL_FENCE_INFOS,
|
|
685
|
+
var ROOT_FILE_NAMES, SHELL_FENCE_INFOS, markdown, COUNT_RE, COUNT_DENYLIST_RE, IGNORE_LINE, IGNORE_START, IGNORE_END;
|
|
806
686
|
var init_claims = __esm({
|
|
807
687
|
"src/audit/claims.ts"() {
|
|
808
688
|
"use strict";
|
|
689
|
+
init_commands();
|
|
809
690
|
init_tree();
|
|
810
691
|
ROOT_FILE_NAMES = /* @__PURE__ */ new Set([
|
|
811
692
|
"package.json",
|
|
@@ -839,7 +720,7 @@ var init_claims = __esm({
|
|
|
839
720
|
".env.example"
|
|
840
721
|
]);
|
|
841
722
|
SHELL_FENCE_INFOS = /* @__PURE__ */ new Set(["", "bash", "sh", "shell", "console", "zsh"]);
|
|
842
|
-
|
|
723
|
+
markdown = new MarkdownIt("commonmark", { html: false });
|
|
843
724
|
COUNT_RE = /(\d+)\s+(modules?|packages?|workspaces?|crates?)\b/gi;
|
|
844
725
|
COUNT_DENYLIST_RE = /^\s*(manager|registr|lock|json)/i;
|
|
845
726
|
IGNORE_LINE = "<!-- mason:ignore -->";
|
|
@@ -849,8 +730,8 @@ var init_claims = __esm({
|
|
|
849
730
|
});
|
|
850
731
|
|
|
851
732
|
// src/audit/git.ts
|
|
852
|
-
import { execFile as
|
|
853
|
-
import { promisify as
|
|
733
|
+
import { execFile as execFile2 } from "child_process";
|
|
734
|
+
import { promisify as promisify2 } from "util";
|
|
854
735
|
function parseCommitLine(line) {
|
|
855
736
|
const parts = line.split(" ");
|
|
856
737
|
if (parts.length < 3 || !parts[0]) return null;
|
|
@@ -858,9 +739,9 @@ function parseCommitLine(line) {
|
|
|
858
739
|
}
|
|
859
740
|
async function lastCommitOf(resolvedRoot, relPath) {
|
|
860
741
|
try {
|
|
861
|
-
const { stdout } = await
|
|
742
|
+
const { stdout } = await exec2(
|
|
862
743
|
"git",
|
|
863
|
-
["log", "-1", `--format=${COMMIT_FORMAT}`, "--", relPath],
|
|
744
|
+
["log", "-1", `--format=${COMMIT_FORMAT}`, "--", `:(literal)${relPath}`],
|
|
864
745
|
{ cwd: resolvedRoot }
|
|
865
746
|
);
|
|
866
747
|
const line = stdout.trim().split("\n")[0];
|
|
@@ -871,7 +752,7 @@ async function lastCommitOf(resolvedRoot, relPath) {
|
|
|
871
752
|
}
|
|
872
753
|
async function deletingCommitOf(resolvedRoot, relPath) {
|
|
873
754
|
try {
|
|
874
|
-
const { stdout } = await
|
|
755
|
+
const { stdout } = await exec2(
|
|
875
756
|
"git",
|
|
876
757
|
[
|
|
877
758
|
"log",
|
|
@@ -879,7 +760,7 @@ async function deletingCommitOf(resolvedRoot, relPath) {
|
|
|
879
760
|
"--diff-filter=D",
|
|
880
761
|
`--format=${COMMIT_FORMAT}`,
|
|
881
762
|
"--",
|
|
882
|
-
relPath
|
|
763
|
+
`:(literal)${relPath}`
|
|
883
764
|
],
|
|
884
765
|
{ cwd: resolvedRoot }
|
|
885
766
|
);
|
|
@@ -891,9 +772,9 @@ async function deletingCommitOf(resolvedRoot, relPath) {
|
|
|
891
772
|
}
|
|
892
773
|
async function firstCommitOf(resolvedRoot, relPath) {
|
|
893
774
|
try {
|
|
894
|
-
const { stdout } = await
|
|
775
|
+
const { stdout } = await exec2(
|
|
895
776
|
"git",
|
|
896
|
-
["log", "--reverse", `--format=${COMMIT_FORMAT}`, "--", relPath],
|
|
777
|
+
["log", "--reverse", `--format=${COMMIT_FORMAT}`, "--", `:(literal)${relPath}`],
|
|
897
778
|
{ cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }
|
|
898
779
|
);
|
|
899
780
|
const line = stdout.trim().split("\n")[0];
|
|
@@ -905,7 +786,7 @@ async function firstCommitOf(resolvedRoot, relPath) {
|
|
|
905
786
|
async function commitsTouchingSince(resolvedRoot, fromHash, pathspecs) {
|
|
906
787
|
if (!fromHash || fromHash === "unknown") return null;
|
|
907
788
|
try {
|
|
908
|
-
const { stdout } = await
|
|
789
|
+
const { stdout } = await exec2(
|
|
909
790
|
"git",
|
|
910
791
|
[
|
|
911
792
|
"log",
|
|
@@ -930,214 +811,48 @@ async function commitsTouchingSince(resolvedRoot, fromHash, pathspecs) {
|
|
|
930
811
|
return null;
|
|
931
812
|
}
|
|
932
813
|
}
|
|
933
|
-
var
|
|
814
|
+
var exec2, COMMIT_FORMAT;
|
|
934
815
|
var init_git = __esm({
|
|
935
816
|
"src/audit/git.ts"() {
|
|
936
817
|
"use strict";
|
|
937
|
-
|
|
818
|
+
exec2 = promisify2(execFile2);
|
|
938
819
|
COMMIT_FORMAT = "%H%x09%cI%x09%s";
|
|
939
820
|
}
|
|
940
821
|
});
|
|
941
822
|
|
|
942
|
-
// src/audit/
|
|
823
|
+
// src/audit/inputs.ts
|
|
943
824
|
import fs4 from "fs/promises";
|
|
944
|
-
import
|
|
945
|
-
import
|
|
946
|
-
import {
|
|
947
|
-
|
|
825
|
+
import * as nativeFs from "fs";
|
|
826
|
+
import path5 from "path";
|
|
827
|
+
import { execFile as execFile3 } from "child_process";
|
|
828
|
+
import { promisify as promisify3 } from "util";
|
|
829
|
+
import fg2 from "fast-glob";
|
|
830
|
+
async function auditInputPath(root, file) {
|
|
831
|
+
if (file === ".") return fs4.realpath(root);
|
|
948
832
|
try {
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
{
|
|
953
|
-
);
|
|
954
|
-
return stdout.trim().length > 0;
|
|
955
|
-
} catch {
|
|
956
|
-
return false;
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
async function discoverDocs(resolvedRoot) {
|
|
960
|
-
const docs = await Promise.all(DOC_CANDIDATES.map(async (candidate) => {
|
|
961
|
-
let content2;
|
|
962
|
-
try {
|
|
963
|
-
content2 = await fs4.readFile(path7.join(resolvedRoot, candidate), "utf-8");
|
|
964
|
-
} catch {
|
|
965
|
-
return null;
|
|
833
|
+
return await storePath(root, file);
|
|
834
|
+
} catch (error) {
|
|
835
|
+
if (error instanceof Error && error.message.startsWith("Symlink")) {
|
|
836
|
+
throw new Error(`Audit input contains a symbolic link: ${file}. Its evidence could not be verified.`, { cause: error });
|
|
966
837
|
}
|
|
967
|
-
|
|
968
|
-
return {
|
|
969
|
-
path: candidate,
|
|
970
|
-
content: content2,
|
|
971
|
-
lineCount: content2.split("\n").length,
|
|
972
|
-
lastCommit,
|
|
973
|
-
dirty,
|
|
974
|
-
claims: extractClaims(content2)
|
|
975
|
-
};
|
|
976
|
-
}));
|
|
977
|
-
return docs.filter((doc) => doc !== null);
|
|
978
|
-
}
|
|
979
|
-
var exec5, DOC_CANDIDATES;
|
|
980
|
-
var init_docs = __esm({
|
|
981
|
-
"src/audit/docs.ts"() {
|
|
982
|
-
"use strict";
|
|
983
|
-
init_claims();
|
|
984
|
-
init_git();
|
|
985
|
-
exec5 = promisify5(execFile5);
|
|
986
|
-
DOC_CANDIDATES = [
|
|
987
|
-
"AGENTS.md",
|
|
988
|
-
"CLAUDE.md",
|
|
989
|
-
".claude/CLAUDE.md"
|
|
990
|
-
];
|
|
991
|
-
}
|
|
992
|
-
});
|
|
993
|
-
|
|
994
|
-
// src/audit/types.ts
|
|
995
|
-
var ALL_CHECKS;
|
|
996
|
-
var init_types = __esm({
|
|
997
|
-
"src/audit/types.ts"() {
|
|
998
|
-
"use strict";
|
|
999
|
-
ALL_CHECKS = [
|
|
1000
|
-
"deleted-reference",
|
|
1001
|
-
"new-module",
|
|
1002
|
-
"stale-count",
|
|
1003
|
-
"dead-command",
|
|
1004
|
-
"deps-changed",
|
|
1005
|
-
"decision-anchor-drift"
|
|
1006
|
-
];
|
|
838
|
+
throw error;
|
|
1007
839
|
}
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
// src/audit/checks/deleted-reference.ts
|
|
1011
|
-
import fs5 from "fs/promises";
|
|
1012
|
-
import path8 from "path";
|
|
1013
|
-
async function exists(absPath) {
|
|
840
|
+
}
|
|
841
|
+
async function readAuditInput(root, file) {
|
|
1014
842
|
try {
|
|
1015
|
-
await
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
async function checkDeletedReferences(ctx) {
|
|
1022
|
-
const result = emptyResult();
|
|
1023
|
-
for (const doc of ctx.docs) {
|
|
1024
|
-
const changes = ctx.changesSinceDoc.get(doc.path);
|
|
1025
|
-
const renames = /* @__PURE__ */ new Map();
|
|
1026
|
-
for (const change of changes ?? []) {
|
|
1027
|
-
if (change.status === "renamed" && change.previousPath) {
|
|
1028
|
-
renames.set(change.previousPath, change.path);
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
for (const claim of doc.claims.paths) {
|
|
1032
|
-
if (claim.path === ".mason" || claim.path.startsWith(".mason/")) {
|
|
1033
|
-
continue;
|
|
1034
|
-
}
|
|
1035
|
-
if (await exists(path8.join(ctx.root, claim.path))) continue;
|
|
1036
|
-
const anchor = { doc: doc.path, line: claim.line, excerpt: claim.excerpt };
|
|
1037
|
-
const renamedTo = renames.get(claim.path) ?? null;
|
|
1038
|
-
if (renamedTo) {
|
|
1039
|
-
result.issues.push({
|
|
1040
|
-
type: "deleted-reference",
|
|
1041
|
-
message: `\`${claim.path}\` was renamed to \`${renamedTo}\``,
|
|
1042
|
-
anchor,
|
|
1043
|
-
confidence: "certain",
|
|
1044
|
-
evidence: {
|
|
1045
|
-
kind: "missing-path",
|
|
1046
|
-
claimed: claim.path,
|
|
1047
|
-
renamedTo,
|
|
1048
|
-
deletedInCommit: null,
|
|
1049
|
-
everTracked: true,
|
|
1050
|
-
parentDirExists: true
|
|
1051
|
-
}
|
|
1052
|
-
});
|
|
1053
|
-
continue;
|
|
1054
|
-
}
|
|
1055
|
-
const tracked = await lastCommitOf(ctx.root, claim.path);
|
|
1056
|
-
if (tracked) {
|
|
1057
|
-
const deleted = await deletingCommitOf(ctx.root, claim.path);
|
|
1058
|
-
const detail = deleted ? ` \u2013 deleted in ${deleted.hash.slice(0, 7)} "${deleted.subject}" (${deleted.date.slice(0, 10)})` : "";
|
|
1059
|
-
result.issues.push({
|
|
1060
|
-
type: "deleted-reference",
|
|
1061
|
-
message: `\`${claim.path}\` no longer exists${detail}`,
|
|
1062
|
-
anchor,
|
|
1063
|
-
confidence: "certain",
|
|
1064
|
-
evidence: {
|
|
1065
|
-
kind: "missing-path",
|
|
1066
|
-
claimed: claim.path,
|
|
1067
|
-
renamedTo: null,
|
|
1068
|
-
deletedInCommit: deleted,
|
|
1069
|
-
everTracked: true,
|
|
1070
|
-
parentDirExists: await exists(
|
|
1071
|
-
path8.join(ctx.root, path8.dirname(claim.path))
|
|
1072
|
-
)
|
|
1073
|
-
}
|
|
1074
|
-
});
|
|
1075
|
-
continue;
|
|
1076
|
-
}
|
|
1077
|
-
const parentDirExists = await exists(
|
|
1078
|
-
path8.join(ctx.root, path8.dirname(claim.path))
|
|
1079
|
-
);
|
|
1080
|
-
if (!parentDirExists) continue;
|
|
1081
|
-
const issue = {
|
|
1082
|
-
type: "deleted-reference",
|
|
1083
|
-
message: `\`${claim.path}\` does not exist (never tracked in git \u2013 possible typo or invented path)`,
|
|
1084
|
-
anchor,
|
|
1085
|
-
confidence: "likely",
|
|
1086
|
-
evidence: {
|
|
1087
|
-
kind: "missing-path",
|
|
1088
|
-
claimed: claim.path,
|
|
1089
|
-
renamedTo: null,
|
|
1090
|
-
deletedInCommit: null,
|
|
1091
|
-
everTracked: false,
|
|
1092
|
-
parentDirExists: true
|
|
1093
|
-
}
|
|
1094
|
-
};
|
|
1095
|
-
result.issues.push(issue);
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
return result;
|
|
1099
|
-
}
|
|
1100
|
-
var init_deleted_reference = __esm({
|
|
1101
|
-
"src/audit/checks/deleted-reference.ts"() {
|
|
1102
|
-
"use strict";
|
|
1103
|
-
init_git();
|
|
1104
|
-
init_checks();
|
|
1105
|
-
}
|
|
1106
|
-
});
|
|
1107
|
-
|
|
1108
|
-
// src/audit/inputs.ts
|
|
1109
|
-
import fs6 from "fs/promises";
|
|
1110
|
-
import * as nativeFs from "fs";
|
|
1111
|
-
import path9 from "path";
|
|
1112
|
-
import { execFile as execFile6 } from "child_process";
|
|
1113
|
-
import { promisify as promisify6 } from "util";
|
|
1114
|
-
import fg2 from "fast-glob";
|
|
1115
|
-
async function auditInputPath(root, file) {
|
|
1116
|
-
if (file === ".") return fs6.realpath(root);
|
|
1117
|
-
try {
|
|
1118
|
-
return await storePath(root, file);
|
|
1119
|
-
} catch (error) {
|
|
1120
|
-
if (error instanceof Error && error.message.startsWith("Symlink")) {
|
|
1121
|
-
throw new Error(`Audit input contains a symbolic link: ${file}. Its evidence could not be verified.`, { cause: error });
|
|
1122
|
-
}
|
|
1123
|
-
throw error;
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
async function readAuditInput(root, file) {
|
|
1127
|
-
try {
|
|
1128
|
-
const value = await readBoundedFile(await auditInputPath(root, file), 10 * 1024 * 1024);
|
|
1129
|
-
if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
|
|
1130
|
-
return value;
|
|
1131
|
-
} catch (error) {
|
|
1132
|
-
if (error.code === "ENOENT") return null;
|
|
1133
|
-
throw error;
|
|
843
|
+
const value = await readBoundedFile(await auditInputPath(root, file), 10 * 1024 * 1024);
|
|
844
|
+
if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
|
|
845
|
+
return value;
|
|
846
|
+
} catch (error) {
|
|
847
|
+
if (error.code === "ENOENT") return null;
|
|
848
|
+
throw error;
|
|
1134
849
|
}
|
|
1135
850
|
}
|
|
1136
851
|
async function auditGlob(root, patterns, options) {
|
|
1137
852
|
if (Array.isArray(patterns) && !patterns.length) return [];
|
|
1138
853
|
const directories = /* @__PURE__ */ new Set();
|
|
1139
854
|
const readdir2 = ((directory, opts, callback) => {
|
|
1140
|
-
const relative =
|
|
855
|
+
const relative = path5.relative(root, directory).split(path5.sep).join("/") || ".";
|
|
1141
856
|
directories.add(relative);
|
|
1142
857
|
if (directories.size > MAX_INPUTS) {
|
|
1143
858
|
callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
|
|
@@ -1148,7 +863,7 @@ async function auditGlob(root, patterns, options) {
|
|
|
1148
863
|
}, callback);
|
|
1149
864
|
});
|
|
1150
865
|
const lstat2 = ((file, callback) => {
|
|
1151
|
-
const relative =
|
|
866
|
+
const relative = path5.relative(root, file).split(path5.sep).join("/") || ".";
|
|
1152
867
|
auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
|
|
1153
868
|
});
|
|
1154
869
|
const stream = fg2.stream(patterns, {
|
|
@@ -1176,7 +891,7 @@ async function auditGlob(root, patterns, options) {
|
|
|
1176
891
|
counts.set(dir, (counts.get(dir) ?? 0) + 1);
|
|
1177
892
|
return counts;
|
|
1178
893
|
}, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
1179
|
-
throw new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} relevant paths. Largest directories: ${largest.map(([dir,
|
|
894
|
+
throw new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} relevant paths. Largest directories: ${largest.map(([dir, count4]) => `${dir} (${count4})`).join(", ")}. Narrow this check's inputs.`);
|
|
1180
895
|
}
|
|
1181
896
|
}
|
|
1182
897
|
} finally {
|
|
@@ -1217,21 +932,460 @@ async function gitIgnoredPaths(root, files) {
|
|
|
1217
932
|
throw error;
|
|
1218
933
|
}
|
|
1219
934
|
}
|
|
1220
|
-
var execute, MAX_INPUTS;
|
|
1221
|
-
var init_inputs = __esm({
|
|
1222
|
-
"src/audit/inputs.ts"() {
|
|
935
|
+
var execute, MAX_INPUTS;
|
|
936
|
+
var init_inputs = __esm({
|
|
937
|
+
"src/audit/inputs.ts"() {
|
|
938
|
+
"use strict";
|
|
939
|
+
init_files();
|
|
940
|
+
init_storage();
|
|
941
|
+
execute = promisify3(execFile3);
|
|
942
|
+
MAX_INPUTS = 1e5;
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
// src/audit/docs.ts
|
|
947
|
+
import fs5 from "fs/promises";
|
|
948
|
+
import path6 from "path";
|
|
949
|
+
import { execFile as execFile4 } from "child_process";
|
|
950
|
+
import { promisify as promisify4 } from "util";
|
|
951
|
+
import fg3 from "fast-glob";
|
|
952
|
+
async function isDirty(resolvedRoot, relPath) {
|
|
953
|
+
try {
|
|
954
|
+
const { stdout } = await exec3(
|
|
955
|
+
"git",
|
|
956
|
+
["status", "--porcelain", "--", `:(literal)${relPath}`],
|
|
957
|
+
{ cwd: resolvedRoot }
|
|
958
|
+
);
|
|
959
|
+
return stdout.trim().length > 0;
|
|
960
|
+
} catch {
|
|
961
|
+
return true;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
async function discoverDocPaths(root) {
|
|
965
|
+
let candidates = [];
|
|
966
|
+
try {
|
|
967
|
+
const { stdout } = await exec3(
|
|
968
|
+
"git",
|
|
969
|
+
[
|
|
970
|
+
"ls-files",
|
|
971
|
+
"-z",
|
|
972
|
+
"--cached",
|
|
973
|
+
"--others",
|
|
974
|
+
"--exclude-standard",
|
|
975
|
+
"--",
|
|
976
|
+
":(icase,glob)**/readme.md",
|
|
977
|
+
":(icase,glob)**/agents.md",
|
|
978
|
+
":(icase,glob)**/claude.md"
|
|
979
|
+
],
|
|
980
|
+
{ cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 }
|
|
981
|
+
);
|
|
982
|
+
candidates = stdout.split("\0").filter(Boolean);
|
|
983
|
+
} catch (error) {
|
|
984
|
+
try {
|
|
985
|
+
await exec3("git", ["rev-parse", "--git-dir"], { cwd: root });
|
|
986
|
+
} catch {
|
|
987
|
+
return localDocPaths(root);
|
|
988
|
+
}
|
|
989
|
+
throw error;
|
|
990
|
+
}
|
|
991
|
+
candidates.push(...await localDocPaths(root));
|
|
992
|
+
const config = await loadProjectConfig(root);
|
|
993
|
+
const excluded = [
|
|
994
|
+
"**/node_modules/**",
|
|
995
|
+
"**/vendor/**",
|
|
996
|
+
"**/dist/**",
|
|
997
|
+
"**/build/**",
|
|
998
|
+
"**/target/**",
|
|
999
|
+
"**/.git/**",
|
|
1000
|
+
"**/.venv/**",
|
|
1001
|
+
"**/venv/**",
|
|
1002
|
+
".mason/**",
|
|
1003
|
+
...config.ignore ?? []
|
|
1004
|
+
];
|
|
1005
|
+
const paths = [];
|
|
1006
|
+
const unique = [...new Set(candidates)].sort();
|
|
1007
|
+
for (let offset = 0; offset < unique.length; offset += 256) {
|
|
1008
|
+
paths.push(...await auditGlob(root, unique.slice(offset, offset + 256).map((p) => fg3.escapePath(p)), {
|
|
1009
|
+
ignore: excluded,
|
|
1010
|
+
followSymbolicLinks: false,
|
|
1011
|
+
label: "Documentation discovery"
|
|
1012
|
+
}));
|
|
1013
|
+
if (paths.length > 1e4) throw new Error("Documentation discovery exceeds 10,000 relevant files; narrow .mason/config.json ignore patterns.");
|
|
1014
|
+
}
|
|
1015
|
+
const priority2 = (file) => {
|
|
1016
|
+
const index2 = DOC_CANDIDATES.findIndex((p) => p.toLowerCase() === file.toLowerCase());
|
|
1017
|
+
return index2 < 0 ? DOC_CANDIDATES.length : index2;
|
|
1018
|
+
};
|
|
1019
|
+
return paths.sort((a, b) => priority2(a) - priority2(b) || a.localeCompare(b));
|
|
1020
|
+
}
|
|
1021
|
+
async function localDocPaths(root) {
|
|
1022
|
+
const paths = [];
|
|
1023
|
+
for (const dir of [".", ".claude"]) {
|
|
1024
|
+
let entries;
|
|
1025
|
+
try {
|
|
1026
|
+
entries = await fs5.readdir(await auditInputPath(root, dir), { withFileTypes: true });
|
|
1027
|
+
} catch (error) {
|
|
1028
|
+
if (error.code === "ENOENT") continue;
|
|
1029
|
+
throw error;
|
|
1030
|
+
}
|
|
1031
|
+
for (const entry of entries) if (/^(?:agents|claude|readme)\.md$/i.test(entry.name)) {
|
|
1032
|
+
paths.push(path6.posix.join(dir, entry.name));
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
return paths;
|
|
1036
|
+
}
|
|
1037
|
+
function documentScope(file) {
|
|
1038
|
+
const parent = path6.posix.dirname(file);
|
|
1039
|
+
return path6.posix.basename(parent).toLowerCase() === ".claude" ? path6.posix.dirname(parent) : parent;
|
|
1040
|
+
}
|
|
1041
|
+
async function discoverDocs(resolvedRoot) {
|
|
1042
|
+
const docs = [];
|
|
1043
|
+
for (const candidate of await discoverDocPaths(resolvedRoot)) {
|
|
1044
|
+
const content2 = await readAuditInput(resolvedRoot, candidate);
|
|
1045
|
+
if (content2 === null) throw new Error("Document disappeared during discovery: " + candidate);
|
|
1046
|
+
const [lastCommit, dirty] = await Promise.all([lastCommitOf(resolvedRoot, candidate), isDirty(resolvedRoot, candidate)]);
|
|
1047
|
+
docs.push({
|
|
1048
|
+
path: candidate,
|
|
1049
|
+
content: content2,
|
|
1050
|
+
scope: documentScope(candidate),
|
|
1051
|
+
kind: /^readme\.md$/i.test(path6.posix.basename(candidate)) ? "readme" : "instructions",
|
|
1052
|
+
lineCount: content2.split("\n").length,
|
|
1053
|
+
lastCommit,
|
|
1054
|
+
dirty,
|
|
1055
|
+
claims: extractClaims(content2)
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
return docs;
|
|
1059
|
+
}
|
|
1060
|
+
var exec3, DOC_CANDIDATES;
|
|
1061
|
+
var init_docs = __esm({
|
|
1062
|
+
"src/audit/docs.ts"() {
|
|
1063
|
+
"use strict";
|
|
1064
|
+
init_claims();
|
|
1065
|
+
init_git();
|
|
1066
|
+
init_inputs();
|
|
1067
|
+
init_files();
|
|
1068
|
+
exec3 = promisify4(execFile4);
|
|
1069
|
+
DOC_CANDIDATES = [
|
|
1070
|
+
"AGENTS.md",
|
|
1071
|
+
"CLAUDE.md",
|
|
1072
|
+
".claude/CLAUDE.md"
|
|
1073
|
+
];
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
// src/audit/scope.ts
|
|
1078
|
+
import fs6 from "fs/promises";
|
|
1079
|
+
import path7 from "path";
|
|
1080
|
+
function scopedPath(directory, value) {
|
|
1081
|
+
if (path7.posix.isAbsolute(value) || value.includes("\\") || /[\x00-\x1f]/.test(value)) return null;
|
|
1082
|
+
const resolved = path7.posix.normalize(path7.posix.join(directory, value));
|
|
1083
|
+
return resolved === ".." || resolved.startsWith("../") ? null : resolved;
|
|
1084
|
+
}
|
|
1085
|
+
function pathClaimScope(doc, claim) {
|
|
1086
|
+
const directory = claim.relativeTo === "document" ? path7.posix.dirname(doc) : documentScope(doc);
|
|
1087
|
+
const local = scopedPath(directory, claim.path);
|
|
1088
|
+
if (!local) return null;
|
|
1089
|
+
const candidates = claim.relativeTo === "document" || directory === "." ? [local] : [...new Set([local, scopedPath(".", claim.path)].filter((p) => p !== null))];
|
|
1090
|
+
return {
|
|
1091
|
+
basis: claim.relativeTo === "document" ? "document-link" : directory === "." ? "repository" : "document",
|
|
1092
|
+
directory,
|
|
1093
|
+
candidates
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
async function pathExists(root, file) {
|
|
1097
|
+
const absolute = await auditInputPath(root, file);
|
|
1098
|
+
try {
|
|
1099
|
+
await fs6.access(absolute);
|
|
1100
|
+
return true;
|
|
1101
|
+
} catch (error) {
|
|
1102
|
+
if (error.code === "ENOENT") return false;
|
|
1103
|
+
throw error;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
function optionalMasonPath(file) {
|
|
1107
|
+
return file.split("/").includes(".mason");
|
|
1108
|
+
}
|
|
1109
|
+
var init_scope = __esm({
|
|
1110
|
+
"src/audit/scope.ts"() {
|
|
1111
|
+
"use strict";
|
|
1112
|
+
init_inputs();
|
|
1113
|
+
init_docs();
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
// src/test-map.ts
|
|
1118
|
+
import path8 from "path";
|
|
1119
|
+
async function buildTestMap(dir) {
|
|
1120
|
+
const rootDir = path8.resolve(dir);
|
|
1121
|
+
const access = await createFileAccess(rootDir);
|
|
1122
|
+
const testPatterns = [
|
|
1123
|
+
"**/*.test.*",
|
|
1124
|
+
"**/*.spec.*",
|
|
1125
|
+
"**/*Test.kt",
|
|
1126
|
+
"**/*Test.java",
|
|
1127
|
+
"**/*Tests.kt",
|
|
1128
|
+
"**/*Tests.java",
|
|
1129
|
+
"**/test_*.py",
|
|
1130
|
+
"**/*_test.py",
|
|
1131
|
+
"**/*_test.go",
|
|
1132
|
+
"**/*Tests.swift",
|
|
1133
|
+
"**/*Test.swift",
|
|
1134
|
+
"**/*_test.rs"
|
|
1135
|
+
];
|
|
1136
|
+
const testFiles = await access.list(testPatterns);
|
|
1137
|
+
const sourceFiles = await access.list();
|
|
1138
|
+
const sourceByBaseName = /* @__PURE__ */ new Map();
|
|
1139
|
+
for (const file of sourceFiles) {
|
|
1140
|
+
if (testFiles.includes(file)) continue;
|
|
1141
|
+
const baseName = path8.basename(file).replace(/\.[^.]+$/, "");
|
|
1142
|
+
const existing = sourceByBaseName.get(baseName) ?? [];
|
|
1143
|
+
existing.push(file);
|
|
1144
|
+
sourceByBaseName.set(baseName, existing);
|
|
1145
|
+
}
|
|
1146
|
+
const paired = [];
|
|
1147
|
+
const unmatched = [];
|
|
1148
|
+
for (const testFile of testFiles) {
|
|
1149
|
+
const testBaseName = path8.basename(testFile).replace(/\.[^.]+$/, "");
|
|
1150
|
+
const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
|
|
1151
|
+
if (!sourceName) {
|
|
1152
|
+
unmatched.push(testFile);
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
const candidates = sourceByBaseName.get(sourceName);
|
|
1156
|
+
if (candidates && candidates.length > 0) {
|
|
1157
|
+
const testDir = path8.dirname(testFile);
|
|
1158
|
+
const bestMatch = candidates.reduce((best, candidate) => {
|
|
1159
|
+
const candidateDir = path8.dirname(candidate);
|
|
1160
|
+
const bestDir = path8.dirname(best);
|
|
1161
|
+
const candidateOverlap = commonSegments(testDir, candidateDir);
|
|
1162
|
+
const bestOverlap = commonSegments(testDir, bestDir);
|
|
1163
|
+
return candidateOverlap > bestOverlap ? candidate : best;
|
|
1164
|
+
});
|
|
1165
|
+
paired.push({
|
|
1166
|
+
test: testFile,
|
|
1167
|
+
source: bestMatch,
|
|
1168
|
+
confidence: candidates.length === 1 ? "exact" : "best-guess"
|
|
1169
|
+
});
|
|
1170
|
+
} else {
|
|
1171
|
+
unmatched.push(testFile);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
return { totalTestFiles: testFiles.length, paired, unmatched };
|
|
1175
|
+
}
|
|
1176
|
+
function commonSegments(pathA, pathB) {
|
|
1177
|
+
const segsA = pathA.split("/");
|
|
1178
|
+
const segsB = pathB.split("/");
|
|
1179
|
+
let count4 = 0;
|
|
1180
|
+
for (let i = 0; i < Math.min(segsA.length, segsB.length); i++) {
|
|
1181
|
+
if (segsA[i] === segsB[i]) count4++;
|
|
1182
|
+
else break;
|
|
1183
|
+
}
|
|
1184
|
+
return count4;
|
|
1185
|
+
}
|
|
1186
|
+
var init_test_map = __esm({
|
|
1187
|
+
"src/test-map.ts"() {
|
|
1188
|
+
"use strict";
|
|
1189
|
+
init_files();
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
// src/snapshot/snapshot.ts
|
|
1194
|
+
import path9 from "path";
|
|
1195
|
+
import { execFile as execFile5 } from "child_process";
|
|
1196
|
+
import { promisify as promisify5 } from "util";
|
|
1197
|
+
import { z as z2 } from "zod";
|
|
1198
|
+
async function inspectSnapshot(rootDir) {
|
|
1199
|
+
try {
|
|
1200
|
+
const raw = await readStoreJson(rootDir, ".mason/snapshot.json");
|
|
1201
|
+
const snapshot = raw === null ? null : snapshotSchema.parse(raw);
|
|
1202
|
+
return { status: snapshot ? "available" : "missing", snapshot, diagnostics: [] };
|
|
1203
|
+
} catch (error) {
|
|
1204
|
+
return { status: "invalid", snapshot: null, diagnostics: [{
|
|
1205
|
+
path: ".mason/snapshot.json",
|
|
1206
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1207
|
+
}] };
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
async function getCurrentGitHash(rootDir) {
|
|
1211
|
+
try {
|
|
1212
|
+
const { stdout } = await exec4("git", ["rev-parse", "HEAD"], {
|
|
1213
|
+
cwd: rootDir
|
|
1214
|
+
});
|
|
1215
|
+
return stdout.trim();
|
|
1216
|
+
} catch {
|
|
1217
|
+
return "unknown";
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
var exec4, repoPath, verificationFields, featureSchema, flowSchema, snapshotSchema;
|
|
1221
|
+
var init_snapshot = __esm({
|
|
1222
|
+
"src/snapshot/snapshot.ts"() {
|
|
1223
|
+
"use strict";
|
|
1224
|
+
init_files();
|
|
1225
|
+
init_files();
|
|
1226
|
+
init_storage();
|
|
1227
|
+
init_paths();
|
|
1228
|
+
init_test_map();
|
|
1229
|
+
exec4 = promisify5(execFile5);
|
|
1230
|
+
repoPath = z2.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
|
|
1231
|
+
verificationFields = {
|
|
1232
|
+
refreshedHash: z2.string().optional(),
|
|
1233
|
+
verifiedAt: z2.string().optional(),
|
|
1234
|
+
verifiedHash: z2.string().optional(),
|
|
1235
|
+
verificationFailed: z2.boolean().optional(),
|
|
1236
|
+
verificationNote: z2.string().optional()
|
|
1237
|
+
};
|
|
1238
|
+
featureSchema = z2.object({
|
|
1239
|
+
description: z2.string(),
|
|
1240
|
+
files: z2.array(repoPath),
|
|
1241
|
+
tests: z2.array(repoPath).optional(),
|
|
1242
|
+
type: z2.enum(["capability", "infrastructure"]).optional(),
|
|
1243
|
+
...verificationFields
|
|
1244
|
+
}).passthrough();
|
|
1245
|
+
flowSchema = z2.object({ description: z2.string(), chain: z2.array(repoPath), ...verificationFields }).passthrough();
|
|
1246
|
+
snapshotSchema = z2.object({
|
|
1247
|
+
version: z2.literal(2),
|
|
1248
|
+
createdAt: z2.string(),
|
|
1249
|
+
updatedAt: z2.string(),
|
|
1250
|
+
gitHash: z2.string(),
|
|
1251
|
+
features: z2.record(featureSchema),
|
|
1252
|
+
flows: z2.record(flowSchema)
|
|
1253
|
+
}).passthrough();
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
// src/drift/drift.ts
|
|
1258
|
+
import fs7 from "fs/promises";
|
|
1259
|
+
import path10 from "path";
|
|
1260
|
+
import { execFile as execFile6 } from "child_process";
|
|
1261
|
+
import { promisify as promisify6 } from "util";
|
|
1262
|
+
function parseChanges(output) {
|
|
1263
|
+
const fields = output.split("\0");
|
|
1264
|
+
const changes = [];
|
|
1265
|
+
for (let i = 0; i < fields.length && fields[i]; ) {
|
|
1266
|
+
const code = fields[i++];
|
|
1267
|
+
const first = fields[i++];
|
|
1268
|
+
if (!first) break;
|
|
1269
|
+
const second = /^[RC]/.test(code) ? fields[i++] : void 0;
|
|
1270
|
+
const change = second ? code.startsWith("R") ? { status: "renamed", path: second, previousPath: first } : { status: "added", path: second } : { status: code === "A" ? "added" : code === "D" ? "deleted" : "modified", path: first };
|
|
1271
|
+
if (change.path.startsWith(".mason/") && (!change.previousPath || change.previousPath.startsWith(".mason/"))) continue;
|
|
1272
|
+
changes.push(change);
|
|
1273
|
+
}
|
|
1274
|
+
return changes;
|
|
1275
|
+
}
|
|
1276
|
+
function touchedPaths(changes) {
|
|
1277
|
+
return [...new Set(changes.flatMap((c) => c.previousPath ? [c.previousPath, c.path] : [c.path]))].sort();
|
|
1278
|
+
}
|
|
1279
|
+
async function getChangesWithStatus(resolvedRoot, fromHash, toHash = "HEAD") {
|
|
1280
|
+
if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-") || !toHash || toHash === "unknown" || toHash.startsWith("-")) return null;
|
|
1281
|
+
try {
|
|
1282
|
+
const { stdout } = await exec5("git", ["diff", "--name-status", "-z", "-M", fromHash, toHash, "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 });
|
|
1283
|
+
return parseChanges(stdout);
|
|
1284
|
+
} catch {
|
|
1285
|
+
return null;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
async function getWorkingTree(resolvedRoot) {
|
|
1289
|
+
try {
|
|
1290
|
+
const [diff, untracked] = await Promise.all([
|
|
1291
|
+
exec5("git", ["diff", "--name-status", "-z", "-M", "HEAD", "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }),
|
|
1292
|
+
exec5("git", ["ls-files", "-z", "--others", "--exclude-standard"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 })
|
|
1293
|
+
]);
|
|
1294
|
+
const untrackedFiles = untracked.stdout.split("\0").filter((f) => f && !f.startsWith(".mason/"));
|
|
1295
|
+
return { available: true, changedFiles: [.../* @__PURE__ */ new Set([...touchedPaths(parseChanges(diff.stdout)), ...untrackedFiles])].sort(), untrackedFiles };
|
|
1296
|
+
} catch {
|
|
1297
|
+
return { available: false, changedFiles: [], untrackedFiles: [] };
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
var exec5;
|
|
1301
|
+
var init_drift = __esm({
|
|
1302
|
+
"src/drift/drift.ts"() {
|
|
1303
|
+
"use strict";
|
|
1304
|
+
init_snapshot();
|
|
1305
|
+
init_paths();
|
|
1306
|
+
exec5 = promisify6(execFile6);
|
|
1307
|
+
}
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
// src/audit/types.ts
|
|
1311
|
+
var ALL_CHECKS;
|
|
1312
|
+
var init_types = __esm({
|
|
1313
|
+
"src/audit/types.ts"() {
|
|
1314
|
+
"use strict";
|
|
1315
|
+
ALL_CHECKS = [
|
|
1316
|
+
"deleted-reference",
|
|
1317
|
+
"new-module",
|
|
1318
|
+
"stale-count",
|
|
1319
|
+
"dead-command",
|
|
1320
|
+
"deps-changed",
|
|
1321
|
+
"decision-anchor-drift"
|
|
1322
|
+
];
|
|
1323
|
+
}
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
// src/audit/checks/deleted-reference.ts
|
|
1327
|
+
import path11 from "path";
|
|
1328
|
+
async function checkDeletedReferences(ctx) {
|
|
1329
|
+
const result = emptyResult();
|
|
1330
|
+
for (const doc of ctx.docs) {
|
|
1331
|
+
const changes = ctx.changesSinceDoc.get(doc.path);
|
|
1332
|
+
const renames = new Map((changes ?? []).filter((c) => c.status === "renamed" && c.previousPath).map((c) => [c.previousPath, c.path]));
|
|
1333
|
+
for (const claim of doc.claims.paths) {
|
|
1334
|
+
const scope = pathClaimScope(doc.path, claim);
|
|
1335
|
+
if (!scope) {
|
|
1336
|
+
result.skipped.push({
|
|
1337
|
+
check: "deleted-reference",
|
|
1338
|
+
doc: doc.path,
|
|
1339
|
+
reason: `Reference ${claim.excerpt} is outside the repository or has an unsupported path.`
|
|
1340
|
+
});
|
|
1341
|
+
continue;
|
|
1342
|
+
}
|
|
1343
|
+
if (scope.candidates.some(optionalMasonPath)) continue;
|
|
1344
|
+
if ((await Promise.all(scope.candidates.map((file) => pathExists(ctx.root, file)))).some(Boolean)) continue;
|
|
1345
|
+
const candidates = await Promise.all(scope.candidates.map(async (file) => ({
|
|
1346
|
+
file,
|
|
1347
|
+
tracked: await lastCommitOf(ctx.root, file),
|
|
1348
|
+
parent: await pathExists(ctx.root, path11.posix.dirname(file))
|
|
1349
|
+
})));
|
|
1350
|
+
const candidate = candidates.find((c) => c.tracked || renames.has(c.file)) ?? candidates.find((c) => c.parent);
|
|
1351
|
+
if (!candidate && claim.relativeTo !== "document") continue;
|
|
1352
|
+
const resolvedPath = candidate?.file ?? scope.candidates[0];
|
|
1353
|
+
const renamedTo = renames.get(resolvedPath) ?? null;
|
|
1354
|
+
const deletedInCommit = candidate?.tracked && !renamedTo ? await deletingCommitOf(ctx.root, resolvedPath) : null;
|
|
1355
|
+
const evidence = {
|
|
1356
|
+
kind: "missing-path",
|
|
1357
|
+
claimed: claim.path,
|
|
1358
|
+
resolvedPath,
|
|
1359
|
+
scope,
|
|
1360
|
+
renamedTo,
|
|
1361
|
+
deletedInCommit,
|
|
1362
|
+
everTracked: !!candidate?.tracked || !!renamedTo,
|
|
1363
|
+
parentDirExists: candidate?.parent ?? false
|
|
1364
|
+
};
|
|
1365
|
+
const anchor = { doc: doc.path, line: claim.line, excerpt: claim.excerpt };
|
|
1366
|
+
const message2 = renamedTo ? `\`${resolvedPath}\` was renamed to \`${renamedTo}\`` : `\`${resolvedPath}\` does not exist` + (deletedInCommit ? ` \u2013 deleted in ${deletedInCommit.hash.slice(0, 7)} "${deletedInCommit.subject}" (${deletedInCommit.date.slice(0, 10)})` : "");
|
|
1367
|
+
if (scope.basis !== "document" && (renamedTo || candidate?.tracked)) {
|
|
1368
|
+
result.issues.push({ type: "deleted-reference", message: message2, anchor, confidence: "certain", evidence });
|
|
1369
|
+
} else {
|
|
1370
|
+
result.advisories.push({ resolution: "recheck", type: "deleted-reference", message: message2 + (scope.basis === "document" ? "; the bare path's document/repository scope needs review." : "; no tracked history establishes it as a required file. Review for a typo, generated output, or example."), anchor, evidence });
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
return result;
|
|
1375
|
+
}
|
|
1376
|
+
var init_deleted_reference = __esm({
|
|
1377
|
+
"src/audit/checks/deleted-reference.ts"() {
|
|
1223
1378
|
"use strict";
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
MAX_INPUTS = 1e5;
|
|
1379
|
+
init_git();
|
|
1380
|
+
init_scope();
|
|
1381
|
+
init_checks();
|
|
1228
1382
|
}
|
|
1229
1383
|
});
|
|
1230
1384
|
|
|
1231
1385
|
// src/audit/checks/new-module.ts
|
|
1232
|
-
import
|
|
1233
|
-
import
|
|
1234
|
-
import
|
|
1386
|
+
import fg4 from "fast-glob";
|
|
1387
|
+
import fs8 from "fs/promises";
|
|
1388
|
+
import path12 from "path";
|
|
1235
1389
|
function escapeRegExp(text2) {
|
|
1236
1390
|
return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1237
1391
|
}
|
|
@@ -1245,15 +1399,15 @@ function isMentioned(combinedDocs, name) {
|
|
|
1245
1399
|
async function checkNewModules(ctx) {
|
|
1246
1400
|
const result = emptyResult();
|
|
1247
1401
|
if (ctx.docs.length === 0) return result;
|
|
1248
|
-
const combinedDocs = ctx.docs
|
|
1402
|
+
const combinedDocs = moduleDocumentation(ctx.docs);
|
|
1249
1403
|
const primaryDoc = ctx.docs[0].path;
|
|
1250
1404
|
const checkedDocs = ctx.docs.map((d) => d.path);
|
|
1251
1405
|
const flag = async (dir, sourceFileCount) => {
|
|
1252
|
-
result.
|
|
1406
|
+
result.advisories.push({
|
|
1407
|
+
resolution: "recheck",
|
|
1253
1408
|
type: "new-module",
|
|
1254
|
-
message: `directory \`${dir}/\` contains ${sourceFileCount} source file${sourceFileCount === 1 ? "" : "s"} but is not mentioned in
|
|
1409
|
+
message: `directory \`${dir}/\` contains ${sourceFileCount} source file${sourceFileCount === 1 ? "" : "s"} but is not mentioned in the discovered documentation; review whether it needs documenting`,
|
|
1255
1410
|
anchor: { doc: primaryDoc, line: null, excerpt: dir },
|
|
1256
|
-
confidence: "likely",
|
|
1257
1411
|
evidence: {
|
|
1258
1412
|
kind: "unmentioned-dir",
|
|
1259
1413
|
dir,
|
|
@@ -1268,41 +1422,44 @@ async function checkNewModules(ctx) {
|
|
|
1268
1422
|
}
|
|
1269
1423
|
return result;
|
|
1270
1424
|
}
|
|
1425
|
+
function moduleDocumentation(docs) {
|
|
1426
|
+
return docs.map((doc) => doc.content + "\n" + (path12.posix.dirname(doc.path) === "." ? "" : path12.posix.dirname(doc.path) + "/")).join("\n");
|
|
1427
|
+
}
|
|
1271
1428
|
async function moduleCandidates(root, combinedDocs) {
|
|
1272
1429
|
const [config, sourcePaths] = await Promise.all([loadProjectConfig(root), gitSourcePaths(root)]);
|
|
1273
1430
|
const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
|
|
1274
1431
|
const sources = sourcePaths.filter((file) => !file.split("/").some((part) => part.startsWith(".")));
|
|
1275
1432
|
const listSubdirs = async (dir) => {
|
|
1276
|
-
const entries = await
|
|
1277
|
-
const dirs = entries.filter((entry) => (entry.isDirectory() || entry.isSymbolicLink()) && !entry.name.startsWith(".") && !DIR_DENYLIST.has(entry.name)).map((entry) =>
|
|
1433
|
+
const entries = await fs8.readdir(await auditInputPath(root, dir), { withFileTypes: true });
|
|
1434
|
+
const dirs = entries.filter((entry) => (entry.isDirectory() || entry.isSymbolicLink() && sources.some((file) => file.startsWith(path12.posix.join(dir, entry.name) + "/"))) && !entry.name.startsWith(".") && !DIR_DENYLIST.has(entry.name)).map((entry) => path12.posix.join(dir, entry.name));
|
|
1278
1435
|
const ignored = await gitIgnoredPaths(root, dirs);
|
|
1279
1436
|
const visible = new Set(dirs.filter((dir2) => !ignored.has(dir2) || sources.some((file) => file.startsWith(dir2 + "/"))));
|
|
1280
|
-
return (await auditGlob(root, dir === "." ? "*" : `${
|
|
1437
|
+
return (await auditGlob(root, dir === "." ? "*" : `${fg4.escapePath(dir)}/*`, {
|
|
1281
1438
|
ignore,
|
|
1282
1439
|
onlyDirectories: true,
|
|
1283
1440
|
label: "Module directory discovery",
|
|
1284
1441
|
select: (file) => visible.has(file)
|
|
1285
|
-
})).map((file) =>
|
|
1442
|
+
})).map((file) => path12.posix.basename(file));
|
|
1286
1443
|
};
|
|
1287
1444
|
const countSourceFiles = async (dir) => {
|
|
1288
1445
|
const files = sources.filter((file) => file.startsWith(dir + "/"));
|
|
1289
|
-
let
|
|
1446
|
+
let count4 = 0;
|
|
1290
1447
|
for (let offset = 0; offset < files.length; offset += 256) {
|
|
1291
|
-
|
|
1448
|
+
count4 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) => fg4.escapePath(file)), {
|
|
1292
1449
|
ignore,
|
|
1293
1450
|
followSymbolicLinks: false,
|
|
1294
1451
|
label: `Source discovery in ${dir}`
|
|
1295
1452
|
})).length;
|
|
1296
|
-
if (
|
|
1453
|
+
if (count4 > 1e5) throw new Error(`Source discovery in ${dir} exceeds 100,000 relevant files. Exclude generated source with .mason/config.json ignore patterns or narrow this audit's checks.`);
|
|
1297
1454
|
}
|
|
1298
|
-
return
|
|
1455
|
+
return count4;
|
|
1299
1456
|
};
|
|
1300
1457
|
const candidates = [];
|
|
1301
1458
|
for (const topDir of await listSubdirs(".")) {
|
|
1302
1459
|
const topMentioned = isMentioned(combinedDocs, topDir);
|
|
1303
1460
|
if (!topMentioned) {
|
|
1304
|
-
const
|
|
1305
|
-
if (
|
|
1461
|
+
const count4 = await countSourceFiles(topDir);
|
|
1462
|
+
if (count4 >= 1) candidates.push({ dir: topDir, sourceFileCount: count4 });
|
|
1306
1463
|
continue;
|
|
1307
1464
|
}
|
|
1308
1465
|
const subdirs = await listSubdirs(topDir);
|
|
@@ -1310,9 +1467,9 @@ async function moduleCandidates(root, combinedDocs) {
|
|
|
1310
1467
|
if (mentioned.length < ENUMERATION_THRESHOLD) continue;
|
|
1311
1468
|
for (const sub of subdirs) {
|
|
1312
1469
|
if (isMentioned(combinedDocs, sub)) continue;
|
|
1313
|
-
const
|
|
1314
|
-
if (
|
|
1315
|
-
candidates.push({ dir: `${topDir}/${sub}`, sourceFileCount:
|
|
1470
|
+
const count4 = await countSourceFiles(`${topDir}/${sub}`);
|
|
1471
|
+
if (count4 >= SECOND_LEVEL_MIN_SOURCE_FILES) {
|
|
1472
|
+
candidates.push({ dir: `${topDir}/${sub}`, sourceFileCount: count4 });
|
|
1316
1473
|
}
|
|
1317
1474
|
}
|
|
1318
1475
|
}
|
|
@@ -1351,7 +1508,7 @@ var init_new_module = __esm({
|
|
|
1351
1508
|
});
|
|
1352
1509
|
|
|
1353
1510
|
// src/audit/checks/stale-count.ts
|
|
1354
|
-
import
|
|
1511
|
+
import path13 from "path";
|
|
1355
1512
|
async function countGradleModules(root) {
|
|
1356
1513
|
for (const name of ["settings.gradle.kts", "settings.gradle"]) {
|
|
1357
1514
|
const content2 = await readAuditInput(root, name);
|
|
@@ -1385,7 +1542,7 @@ async function countNpmWorkspaces(root) {
|
|
|
1385
1542
|
return {
|
|
1386
1543
|
actual: matched.length,
|
|
1387
1544
|
countedFrom: "package.json workspaces",
|
|
1388
|
-
members: matched.map((m) =>
|
|
1545
|
+
members: matched.map((m) => path13.dirname(m)).sort()
|
|
1389
1546
|
};
|
|
1390
1547
|
}
|
|
1391
1548
|
}
|
|
@@ -1416,7 +1573,7 @@ async function countNpmWorkspaces(root) {
|
|
|
1416
1573
|
return {
|
|
1417
1574
|
actual: matched.length,
|
|
1418
1575
|
countedFrom: "pnpm-workspace.yaml",
|
|
1419
|
-
members: matched.map((m) =>
|
|
1576
|
+
members: matched.map((m) => path13.dirname(m)).sort()
|
|
1420
1577
|
};
|
|
1421
1578
|
}
|
|
1422
1579
|
}
|
|
@@ -1438,8 +1595,8 @@ async function countCargoCrates(root) {
|
|
|
1438
1595
|
ignore: ["**/target/**"],
|
|
1439
1596
|
label: "Cargo workspace discovery"
|
|
1440
1597
|
});
|
|
1441
|
-
for (const m of matched) members.add(
|
|
1442
|
-
} else if (await readAuditInput(root,
|
|
1598
|
+
for (const m of matched) members.add(path13.dirname(m));
|
|
1599
|
+
} else if (await readAuditInput(root, path13.posix.join(entry, "Cargo.toml")) !== null) {
|
|
1443
1600
|
members.add(entry);
|
|
1444
1601
|
}
|
|
1445
1602
|
}
|
|
@@ -1457,11 +1614,21 @@ async function resolveCountSource(root, claim) {
|
|
|
1457
1614
|
if (unit === "crate") return countCargoCrates(root);
|
|
1458
1615
|
return await countNpmWorkspaces(root) ?? await countCargoCrates(root) ?? await countGradleModules(root);
|
|
1459
1616
|
}
|
|
1617
|
+
async function resolveDocCountSource(root, doc, claim) {
|
|
1618
|
+
const scope = documentScope(doc);
|
|
1619
|
+
const source = await resolveCountSource(path13.join(root, scope), claim);
|
|
1620
|
+
if (!source || scope === ".") return source;
|
|
1621
|
+
return {
|
|
1622
|
+
...source,
|
|
1623
|
+
countedFrom: scope + "/" + source.countedFrom,
|
|
1624
|
+
members: source.members.map((member) => scope + "/" + member)
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1460
1627
|
async function checkStaleCounts(ctx) {
|
|
1461
1628
|
const result = emptyResult();
|
|
1462
1629
|
for (const doc of ctx.docs) {
|
|
1463
1630
|
for (const claim of doc.claims.counts) {
|
|
1464
|
-
const source = await
|
|
1631
|
+
const source = await resolveDocCountSource(ctx.root, doc.path, claim);
|
|
1465
1632
|
if (source === null) {
|
|
1466
1633
|
result.skipped.push({
|
|
1467
1634
|
check: "stale-count",
|
|
@@ -1493,6 +1660,7 @@ var MEMBERS_CAP;
|
|
|
1493
1660
|
var init_stale_count = __esm({
|
|
1494
1661
|
"src/audit/checks/stale-count.ts"() {
|
|
1495
1662
|
"use strict";
|
|
1663
|
+
init_docs();
|
|
1496
1664
|
init_inputs();
|
|
1497
1665
|
init_checks();
|
|
1498
1666
|
MEMBERS_CAP = 50;
|
|
@@ -1500,61 +1668,94 @@ var init_stale_count = __esm({
|
|
|
1500
1668
|
});
|
|
1501
1669
|
|
|
1502
1670
|
// src/audit/checks/dead-command.ts
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
if (raw === null) return null;
|
|
1671
|
+
import path14 from "path";
|
|
1672
|
+
function scriptsOf(raw) {
|
|
1506
1673
|
try {
|
|
1507
1674
|
const pkg = JSON.parse(raw);
|
|
1508
|
-
|
|
1675
|
+
if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) return null;
|
|
1676
|
+
if (pkg.scripts === void 0) return [];
|
|
1677
|
+
return pkg.scripts && typeof pkg.scripts === "object" && !Array.isArray(pkg.scripts) && Object.values(pkg.scripts).every((value) => typeof value === "string") ? Object.keys(pkg.scripts) : null;
|
|
1509
1678
|
} catch {
|
|
1510
1679
|
return null;
|
|
1511
1680
|
}
|
|
1512
1681
|
}
|
|
1682
|
+
async function commandInputs(root, docs) {
|
|
1683
|
+
const contents = /* @__PURE__ */ new Map();
|
|
1684
|
+
const read = async (file) => {
|
|
1685
|
+
if (!contents.has(file)) contents.set(file, await readAuditInput(root, file));
|
|
1686
|
+
return contents.get(file);
|
|
1687
|
+
};
|
|
1688
|
+
await read("package.json");
|
|
1689
|
+
let inventory;
|
|
1690
|
+
const resolutions = [];
|
|
1691
|
+
for (const doc of docs) for (const claim of doc.claims.commands) {
|
|
1692
|
+
const base = documentScope(doc.path);
|
|
1693
|
+
const directory = scopedPath(base, claim.directory ?? ".");
|
|
1694
|
+
const scope = {
|
|
1695
|
+
basis: claim.directory === void 0 ? "document" : "explicit",
|
|
1696
|
+
directory: directory ?? base,
|
|
1697
|
+
candidates: []
|
|
1698
|
+
};
|
|
1699
|
+
const resolution = { doc: doc.path, claim, scope, manifest: null, scripts: [], elsewhere: [] };
|
|
1700
|
+
resolutions.push(resolution);
|
|
1701
|
+
if (claim.scopeUnknown || directory === null) {
|
|
1702
|
+
resolution.problem = claim.scopeUnknown ?? "The command's directory leaves the repository.";
|
|
1703
|
+
continue;
|
|
1704
|
+
}
|
|
1705
|
+
let cursor = directory;
|
|
1706
|
+
while (true) {
|
|
1707
|
+
const manifest = path14.posix.join(cursor, "package.json");
|
|
1708
|
+
scope.candidates.push(manifest);
|
|
1709
|
+
const raw = await read(manifest);
|
|
1710
|
+
if (raw !== null) {
|
|
1711
|
+
resolution.manifest = manifest;
|
|
1712
|
+
const scripts = scriptsOf(raw);
|
|
1713
|
+
if (scripts === null) resolution.problem = "Invalid package manifest: " + manifest;
|
|
1714
|
+
else resolution.scripts = scripts;
|
|
1715
|
+
break;
|
|
1716
|
+
}
|
|
1717
|
+
if (claim.directory !== void 0 || cursor === ".") break;
|
|
1718
|
+
cursor = path14.posix.dirname(cursor);
|
|
1719
|
+
}
|
|
1720
|
+
if (!resolution.manifest && !resolution.problem) {
|
|
1721
|
+
resolution.problem = `No package.json for the command's scope (${directory}).`;
|
|
1722
|
+
}
|
|
1723
|
+
if (resolution.problem || resolution.scripts.includes(claim.scriptName) || claim.directory !== void 0) continue;
|
|
1724
|
+
inventory ??= await commandManifests(root);
|
|
1725
|
+
for (const manifest of [.../* @__PURE__ */ new Set(["package.json", ...inventory])].sort()) {
|
|
1726
|
+
if (scope.candidates.includes(manifest)) continue;
|
|
1727
|
+
scope.candidates.push(manifest);
|
|
1728
|
+
const raw = await read(manifest);
|
|
1729
|
+
if (raw === null) continue;
|
|
1730
|
+
const scripts = scriptsOf(raw);
|
|
1731
|
+
if (scripts === null) resolution.problem = "Cannot establish script availability: invalid manifest " + manifest;
|
|
1732
|
+
else if (scripts.includes(claim.scriptName)) resolution.elsewhere.push(manifest);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
return { manifests: [...contents].sort(([a], [b]) => a.localeCompare(b)), resolutions };
|
|
1736
|
+
}
|
|
1513
1737
|
async function checkDeadCommands(ctx) {
|
|
1514
1738
|
const result = emptyResult();
|
|
1515
|
-
const
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
if (rootScripts === null) {
|
|
1521
|
-
result.skipped.push({
|
|
1522
|
-
check: "dead-command",
|
|
1523
|
-
reason: "no package.json at the repo root"
|
|
1524
|
-
});
|
|
1525
|
-
return result;
|
|
1526
|
-
}
|
|
1527
|
-
const rootSet = new Set(rootScripts);
|
|
1528
|
-
let workspaceScripts = null;
|
|
1529
|
-
let manifestsChecked = ["package.json"];
|
|
1530
|
-
const loadWorkspaceScripts = async () => {
|
|
1531
|
-
if (workspaceScripts !== null) return workspaceScripts;
|
|
1532
|
-
workspaceScripts = /* @__PURE__ */ new Set();
|
|
1533
|
-
const manifests = await commandManifests(ctx.root);
|
|
1534
|
-
manifestsChecked = ["package.json", ...manifests.sort()];
|
|
1535
|
-
for (const manifest of manifests) {
|
|
1536
|
-
const scripts = await scriptsOf(ctx.root, manifest);
|
|
1537
|
-
for (const name of scripts ?? []) workspaceScripts.add(name);
|
|
1739
|
+
const { resolutions } = await commandInputs(ctx.root, ctx.docs);
|
|
1740
|
+
for (const { doc, claim, scope, scripts, elsewhere, problem, manifest } of resolutions) {
|
|
1741
|
+
if (problem) {
|
|
1742
|
+
result.skipped.push({ check: "dead-command", doc, reason: problem });
|
|
1743
|
+
continue;
|
|
1538
1744
|
}
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
invocation: claim.invocation,
|
|
1554
|
-
manifestsChecked,
|
|
1555
|
-
availableScripts: rootScripts.slice(0, AVAILABLE_SCRIPTS_CAP)
|
|
1556
|
-
}
|
|
1557
|
-
});
|
|
1745
|
+
if (scripts.includes(claim.scriptName)) continue;
|
|
1746
|
+
const evidence = {
|
|
1747
|
+
kind: "missing-script",
|
|
1748
|
+
scriptName: claim.scriptName,
|
|
1749
|
+
invocation: claim.invocation,
|
|
1750
|
+
manifestsChecked: scope.candidates,
|
|
1751
|
+
availableScripts: scripts.slice(0, AVAILABLE_SCRIPTS_CAP),
|
|
1752
|
+
scope
|
|
1753
|
+
};
|
|
1754
|
+
const anchor = { doc, line: claim.line, excerpt: claim.excerpt };
|
|
1755
|
+
const message2 = `\`${claim.invocation}\` names script "${claim.scriptName}", which is absent from ${manifest}`;
|
|
1756
|
+
const certain = documentScope(doc) === "." && (scope.basis === "explicit" || !elsewhere.length);
|
|
1757
|
+
if (certain) result.issues.push({ type: "dead-command", message: message2, anchor, confidence: "certain", evidence });
|
|
1758
|
+
else result.advisories.push({ resolution: "recheck", type: "dead-command", message: message2 + (elsewhere.length ? `; it exists in ${elsewhere.join(", ")}. Clarify the intended working directory.` : "; the intended working directory needs review."), anchor, evidence });
|
|
1558
1759
|
}
|
|
1559
1760
|
return result;
|
|
1560
1761
|
}
|
|
@@ -1569,6 +1770,8 @@ var init_dead_command = __esm({
|
|
|
1569
1770
|
"src/audit/checks/dead-command.ts"() {
|
|
1570
1771
|
"use strict";
|
|
1571
1772
|
init_inputs();
|
|
1773
|
+
init_docs();
|
|
1774
|
+
init_scope();
|
|
1572
1775
|
init_checks();
|
|
1573
1776
|
AVAILABLE_SCRIPTS_CAP = 30;
|
|
1574
1777
|
}
|
|
@@ -1582,13 +1785,13 @@ async function git(root, args) {
|
|
|
1582
1785
|
}
|
|
1583
1786
|
function withoutAndroidReleaseValues(text2) {
|
|
1584
1787
|
if (/\/\*|"""|'''/.test(text2) || !/id\s*\(?\s*["']com\.android\.(application|library)["']/.test(text2)) return null;
|
|
1585
|
-
const
|
|
1788
|
+
const scopes2 = [];
|
|
1586
1789
|
const normalized = [];
|
|
1587
1790
|
let assignments = 0;
|
|
1588
1791
|
for (const line of text2.split("\n")) {
|
|
1589
1792
|
const code = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\/\/.*$/g, (token) => token.startsWith("//") ? "" : '""');
|
|
1590
1793
|
const assignment = line.match(/^(\s*)(versionName|versionCode)(\s*(?:=\s*|\s+))("[A-Za-z0-9._+-]+"|'[A-Za-z0-9._+-]+'|\d+)(\s*)$/);
|
|
1591
|
-
if (assignment &&
|
|
1794
|
+
if (assignment && scopes2.join("/") === "android/defaultConfig" && (assignment[2] === "versionCode" ? /^\d+$/.test(assignment[4]) : /^["']/.test(assignment[4]))) {
|
|
1592
1795
|
normalized.push(assignment[1] + assignment[2] + assignment[3] + "<release-value>" + assignment[5]);
|
|
1593
1796
|
assignments++;
|
|
1594
1797
|
} else {
|
|
@@ -1597,12 +1800,12 @@ function withoutAndroidReleaseValues(text2) {
|
|
|
1597
1800
|
}
|
|
1598
1801
|
const named = code.match(/^\s*(android|defaultConfig)\s*\{\s*$/)?.[1];
|
|
1599
1802
|
for (const brace of code.matchAll(/[{}]/g)) {
|
|
1600
|
-
if (brace[0] === "{")
|
|
1601
|
-
else if (!
|
|
1602
|
-
else
|
|
1803
|
+
if (brace[0] === "{") scopes2.push(named ?? "unknown");
|
|
1804
|
+
else if (!scopes2.length) return null;
|
|
1805
|
+
else scopes2.pop();
|
|
1603
1806
|
}
|
|
1604
1807
|
}
|
|
1605
|
-
return assignments && !
|
|
1808
|
+
return assignments && !scopes2.length ? normalized.join("\n") : null;
|
|
1606
1809
|
}
|
|
1607
1810
|
async function releaseMetadataOnly(root, commit) {
|
|
1608
1811
|
if (!commit.files.length || !commit.files.every((file) => /(^|\/)build\.gradle(?:\.kts)?$/.test(file))) return false;
|
|
@@ -1633,6 +1836,12 @@ var init_release_metadata = __esm({
|
|
|
1633
1836
|
});
|
|
1634
1837
|
|
|
1635
1838
|
// src/audit/checks/deps-changed.ts
|
|
1839
|
+
function manifestPathspecs(doc) {
|
|
1840
|
+
const scope = documentScope(doc);
|
|
1841
|
+
if (scope === ".") return MANIFEST_PATHSPECS;
|
|
1842
|
+
const escaped = scope.replace(/[\\*?\[\]]/g, "\\$&");
|
|
1843
|
+
return MANIFEST_PATHSPECS.map((spec) => spec.startsWith(":(glob)") ? ":(glob)" + escaped + "/" + spec.slice(":(glob)".length) : ":(literal)" + scope + "/" + spec);
|
|
1844
|
+
}
|
|
1636
1845
|
async function checkDepsChanged(ctx) {
|
|
1637
1846
|
const result = emptyResult();
|
|
1638
1847
|
result.suppressedAdvisories = [];
|
|
@@ -1656,7 +1865,7 @@ async function checkDepsChanged(ctx) {
|
|
|
1656
1865
|
const range = await commitsTouchingSince(
|
|
1657
1866
|
ctx.root,
|
|
1658
1867
|
doc.lastCommit.hash,
|
|
1659
|
-
|
|
1868
|
+
manifestPathspecs(doc.path)
|
|
1660
1869
|
);
|
|
1661
1870
|
if (range === null) {
|
|
1662
1871
|
result.skipped.push({
|
|
@@ -1695,6 +1904,7 @@ var MANIFEST_COMMITS_CAP, MANIFEST_PATHSPECS;
|
|
|
1695
1904
|
var init_deps_changed = __esm({
|
|
1696
1905
|
"src/audit/checks/deps-changed.ts"() {
|
|
1697
1906
|
"use strict";
|
|
1907
|
+
init_docs();
|
|
1698
1908
|
init_release_metadata();
|
|
1699
1909
|
init_git();
|
|
1700
1910
|
init_checks();
|
|
@@ -1717,7 +1927,7 @@ var init_deps_changed = __esm({
|
|
|
1717
1927
|
});
|
|
1718
1928
|
|
|
1719
1929
|
// src/context/lexical.ts
|
|
1720
|
-
import
|
|
1930
|
+
import path15 from "path";
|
|
1721
1931
|
var init_lexical = __esm({
|
|
1722
1932
|
"src/context/lexical.ts"() {
|
|
1723
1933
|
"use strict";
|
|
@@ -1900,15 +2110,16 @@ var init_provenance = __esm({
|
|
|
1900
2110
|
});
|
|
1901
2111
|
|
|
1902
2112
|
// src/decisions/decisions.ts
|
|
1903
|
-
import
|
|
1904
|
-
import
|
|
2113
|
+
import fs9 from "fs/promises";
|
|
2114
|
+
import path16 from "path";
|
|
1905
2115
|
import { createHash } from "crypto";
|
|
2116
|
+
import { z as z4 } from "zod";
|
|
1906
2117
|
async function loadDecisionStore(rootDir) {
|
|
1907
2118
|
const records = [];
|
|
1908
2119
|
const diagnostics = [];
|
|
1909
2120
|
let entries;
|
|
1910
2121
|
try {
|
|
1911
|
-
entries = await
|
|
2122
|
+
entries = await fs9.readdir(await storePath(rootDir, ".mason/decisions"));
|
|
1912
2123
|
} catch (error) {
|
|
1913
2124
|
if (error.code !== "ENOENT") diagnostics.push({ path: ".mason/decisions", message: String(error) });
|
|
1914
2125
|
return { records, diagnostics };
|
|
@@ -1926,6 +2137,7 @@ async function loadDecisionStore(rootDir) {
|
|
|
1926
2137
|
}
|
|
1927
2138
|
return { records, diagnostics };
|
|
1928
2139
|
}
|
|
2140
|
+
var TITLE_MAX_CHARS, BODY_RECOMMENDED_CHARS, BODY_MAX_CHARS, boundedText, decisionTitleSchema, decisionBodySchema;
|
|
1929
2141
|
var init_decisions = __esm({
|
|
1930
2142
|
"src/decisions/decisions.ts"() {
|
|
1931
2143
|
"use strict";
|
|
@@ -1934,13 +2146,25 @@ var init_decisions = __esm({
|
|
|
1934
2146
|
init_snapshot();
|
|
1935
2147
|
init_lexical();
|
|
1936
2148
|
init_provenance();
|
|
2149
|
+
TITLE_MAX_CHARS = 80;
|
|
2150
|
+
BODY_RECOMMENDED_CHARS = 1500;
|
|
2151
|
+
BODY_MAX_CHARS = 2500;
|
|
2152
|
+
boundedText = (field, max, hint) => z4.string({
|
|
2153
|
+
errorMap: (issue, ctx) => ({ message: issue.code === "too_big" ? `${field} has ${ctx.data.length} characters; maximum is ${max} (${ctx.data.length - max} over). ${hint}` : ctx.defaultError })
|
|
2154
|
+
}).trim().min(1, `${field} must be non-empty`).max(max);
|
|
2155
|
+
decisionTitleSchema = boundedText("title", TITLE_MAX_CHARS, "Tighten it to a specific headline.");
|
|
2156
|
+
decisionBodySchema = boundedText(
|
|
2157
|
+
"body",
|
|
2158
|
+
BODY_MAX_CHARS,
|
|
2159
|
+
`Aim for ${BODY_RECOMMENDED_CHARS} characters or fewer. Preserve the rule, reason, and exceptions; put supporting references in sources.`
|
|
2160
|
+
);
|
|
1937
2161
|
}
|
|
1938
2162
|
});
|
|
1939
2163
|
|
|
1940
2164
|
// src/decisions/drift.ts
|
|
1941
|
-
import
|
|
2165
|
+
import path17 from "path";
|
|
1942
2166
|
async function computeDecisionDrift(rootDir, decisions) {
|
|
1943
|
-
const resolvedRoot =
|
|
2167
|
+
const resolvedRoot = path17.resolve(rootDir);
|
|
1944
2168
|
const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
|
|
1945
2169
|
const report = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
|
|
1946
2170
|
const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
|
|
@@ -2056,11 +2280,303 @@ var init_checks = __esm({
|
|
|
2056
2280
|
}
|
|
2057
2281
|
});
|
|
2058
2282
|
|
|
2283
|
+
// src/audit/findings.ts
|
|
2284
|
+
import { createHash as createHash2 } from "crypto";
|
|
2285
|
+
import { z as z5 } from "zod";
|
|
2286
|
+
function findingId(finding) {
|
|
2287
|
+
const e = finding.evidence;
|
|
2288
|
+
let key;
|
|
2289
|
+
switch (e.kind) {
|
|
2290
|
+
case "missing-path":
|
|
2291
|
+
key = e.resolvedPath && e.resolvedPath !== e.claimed ? [e.claimed, e.resolvedPath] : e.claimed;
|
|
2292
|
+
break;
|
|
2293
|
+
case "unmentioned-dir":
|
|
2294
|
+
key = e.dir;
|
|
2295
|
+
break;
|
|
2296
|
+
case "count-mismatch":
|
|
2297
|
+
key = [e.unit.replace(/s$/, ""), e.countedFrom];
|
|
2298
|
+
break;
|
|
2299
|
+
case "missing-script":
|
|
2300
|
+
key = e.scope && e.scope.directory !== "." ? [e.scriptName, e.scope.directory] : e.scriptName;
|
|
2301
|
+
break;
|
|
2302
|
+
case "doc-behind-manifests":
|
|
2303
|
+
key = null;
|
|
2304
|
+
break;
|
|
2305
|
+
case "decision-anchor":
|
|
2306
|
+
key = [e.decisionId, e.provenance?.revision, e.provenance?.approval];
|
|
2307
|
+
break;
|
|
2308
|
+
}
|
|
2309
|
+
return digest([finding.type, finding.anchor.doc, key]);
|
|
2310
|
+
}
|
|
2311
|
+
var digest, checkSchema, commitSchema, anchorSchema, count, scopeSchema, evidenceSchema, findingSchema, issueSchema, advisorySchema, checkResultSchema;
|
|
2312
|
+
var init_findings = __esm({
|
|
2313
|
+
"src/audit/findings.ts"() {
|
|
2314
|
+
"use strict";
|
|
2315
|
+
digest = (value) => createHash2("sha256").update(JSON.stringify(value)).digest("hex");
|
|
2316
|
+
checkSchema = z5.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"]);
|
|
2317
|
+
commitSchema = z5.object({ hash: z5.string().regex(/^[a-f0-9]{40,64}$/), date: z5.string(), subject: z5.string() });
|
|
2318
|
+
anchorSchema = z5.object({ doc: z5.string(), line: z5.number().int().positive().nullable(), excerpt: z5.string().nullable() });
|
|
2319
|
+
count = z5.number().int().nonnegative();
|
|
2320
|
+
scopeSchema = z5.object({
|
|
2321
|
+
basis: z5.enum(["repository", "document", "document-link", "explicit"]),
|
|
2322
|
+
directory: z5.string(),
|
|
2323
|
+
candidates: z5.array(z5.string())
|
|
2324
|
+
});
|
|
2325
|
+
evidenceSchema = z5.discriminatedUnion("kind", [
|
|
2326
|
+
z5.object({
|
|
2327
|
+
kind: z5.literal("missing-path"),
|
|
2328
|
+
claimed: z5.string(),
|
|
2329
|
+
renamedTo: z5.string().nullable(),
|
|
2330
|
+
deletedInCommit: commitSchema.nullable(),
|
|
2331
|
+
everTracked: z5.boolean(),
|
|
2332
|
+
parentDirExists: z5.boolean(),
|
|
2333
|
+
resolvedPath: z5.string().optional(),
|
|
2334
|
+
scope: scopeSchema.optional()
|
|
2335
|
+
}),
|
|
2336
|
+
z5.object({
|
|
2337
|
+
kind: z5.literal("unmentioned-dir"),
|
|
2338
|
+
dir: z5.string(),
|
|
2339
|
+
sourceFileCount: count,
|
|
2340
|
+
firstCommit: commitSchema.nullable(),
|
|
2341
|
+
checkedDocs: z5.array(z5.string())
|
|
2342
|
+
}),
|
|
2343
|
+
z5.object({
|
|
2344
|
+
kind: z5.literal("count-mismatch"),
|
|
2345
|
+
claimed: count,
|
|
2346
|
+
actual: count,
|
|
2347
|
+
unit: z5.string(),
|
|
2348
|
+
countedFrom: z5.string(),
|
|
2349
|
+
members: z5.array(z5.string())
|
|
2350
|
+
}),
|
|
2351
|
+
z5.object({
|
|
2352
|
+
kind: z5.literal("missing-script"),
|
|
2353
|
+
scriptName: z5.string(),
|
|
2354
|
+
invocation: z5.string(),
|
|
2355
|
+
manifestsChecked: z5.array(z5.string()),
|
|
2356
|
+
availableScripts: z5.array(z5.string()),
|
|
2357
|
+
scope: scopeSchema.optional()
|
|
2358
|
+
}),
|
|
2359
|
+
z5.object({
|
|
2360
|
+
kind: z5.literal("doc-behind-manifests"),
|
|
2361
|
+
docLastCommit: commitSchema,
|
|
2362
|
+
manifestCommits: z5.array(commitSchema.extend({ files: z5.array(z5.string()) })),
|
|
2363
|
+
totalCommits: count
|
|
2364
|
+
}),
|
|
2365
|
+
z5.object({
|
|
2366
|
+
kind: z5.literal("decision-anchor"),
|
|
2367
|
+
decisionId: z5.string(),
|
|
2368
|
+
title: z5.string(),
|
|
2369
|
+
changedFiles: z5.array(z5.string()),
|
|
2370
|
+
refreshedHash: z5.string(),
|
|
2371
|
+
provenance: z5.object({}).passthrough().optional()
|
|
2372
|
+
})
|
|
2373
|
+
]);
|
|
2374
|
+
findingSchema = z5.object({ message: z5.string(), anchor: anchorSchema, evidence: evidenceSchema });
|
|
2375
|
+
issueSchema = findingSchema.extend({
|
|
2376
|
+
type: z5.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
|
|
2377
|
+
confidence: z5.enum(["certain", "likely"])
|
|
2378
|
+
});
|
|
2379
|
+
advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z5.literal("recheck").optional() });
|
|
2380
|
+
checkResultSchema = z5.object({
|
|
2381
|
+
issues: z5.array(issueSchema),
|
|
2382
|
+
advisories: z5.array(advisorySchema),
|
|
2383
|
+
suppressedAdvisories: z5.array(advisorySchema).optional(),
|
|
2384
|
+
skipped: z5.array(z5.object({ check: z5.string(), reason: z5.string(), doc: z5.string().optional() }))
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2387
|
+
});
|
|
2388
|
+
|
|
2389
|
+
// src/audit/advisory-review.ts
|
|
2390
|
+
import fs10 from "fs/promises";
|
|
2391
|
+
import { execFile as execFile8 } from "child_process";
|
|
2392
|
+
import { promisify as promisify8 } from "util";
|
|
2393
|
+
import { z as z6 } from "zod";
|
|
2394
|
+
async function readRecord(root, id) {
|
|
2395
|
+
const raw = await readStoreJson(root, recordPath(id));
|
|
2396
|
+
if (raw === null) return null;
|
|
2397
|
+
const record = recordSchema.parse(raw);
|
|
2398
|
+
const { digest: stored, ...payload } = record;
|
|
2399
|
+
if (record.id !== id || digest(payload) !== stored || record.events.some((event) => findingId(event.finding) !== id || event.evidence.fingerprint !== digest([event.evidence.scope, event.evidence.tree, event.evidence.lastScopeCommit, event.evidence.paths]))) {
|
|
2400
|
+
throw new Error("Advisory review record is inconsistent or modified; inspect its history.");
|
|
2401
|
+
}
|
|
2402
|
+
return record;
|
|
2403
|
+
}
|
|
2404
|
+
function scopes(finding) {
|
|
2405
|
+
const files = [finding.anchor.doc], e = finding.evidence;
|
|
2406
|
+
if (e.kind === "decision-anchor") throw new Error("Use review_decision for decision acceptance, reaffirmation or retirement.");
|
|
2407
|
+
if (e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`, ...manifestPathspecs(finding.anchor.doc)];
|
|
2408
|
+
if (e.kind === "missing-path") files.push(...e.scope?.candidates ?? [e.resolvedPath ?? e.claimed]);
|
|
2409
|
+
else if (e.kind === "missing-script") files.push(...e.manifestsChecked, ...e.scope?.candidates ?? []);
|
|
2410
|
+
else if (e.kind === "unmentioned-dir") files.push(e.dir, ...e.checkedDocs);
|
|
2411
|
+
else throw new Error("This advisory has no supported review scope.");
|
|
2412
|
+
return [...new Set(files)].map((file) => {
|
|
2413
|
+
if (normalizeRepoPath(file) !== file) throw new Error("Advisory contains an invalid scoped path: " + file);
|
|
2414
|
+
return `:(literal)${file}`;
|
|
2415
|
+
}).sort();
|
|
2416
|
+
}
|
|
2417
|
+
async function reviewEvidence(root, finding) {
|
|
2418
|
+
if (normalizeRepoPath(finding.anchor.doc) !== finding.anchor.doc) throw new Error("Invalid advisory document path.");
|
|
2419
|
+
const scope = scopes(finding);
|
|
2420
|
+
const head = (await git2(root, "rev-parse", "HEAD")).trim();
|
|
2421
|
+
const tracked = (await git2(root, "ls-files", "--cached", "-z", "--", ...scope)).split("\0").filter(Boolean);
|
|
2422
|
+
if (tracked.length > 2e4) throw new Error("Advisory scope exceeds 20,000 tracked files.");
|
|
2423
|
+
const [tree, status, lastScopeCommit, document] = await Promise.all([
|
|
2424
|
+
tracked.length ? git2(root, "ls-tree", "-r", "-z", "HEAD", "--", ...tracked.map((file) => `:(literal)${file}`)) : "",
|
|
2425
|
+
git2(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...scope),
|
|
2426
|
+
git2(root, "log", "-1", "--format=%H", "HEAD", "--", ...scope).then((value) => value.trim()),
|
|
2427
|
+
readAuditInput(root, finding.anchor.doc)
|
|
2428
|
+
]);
|
|
2429
|
+
if (!document?.trim()) throw new Error("Original advisory document is missing, empty or unreadable.");
|
|
2430
|
+
if (tree.length > 2 * 1024 * 1024 || tree.split("\0").length > 2e4) throw new Error("Advisory scope is too large for a complete review; narrow the documentation scope.");
|
|
2431
|
+
for (const entry of tree.split("\0").filter(Boolean)) {
|
|
2432
|
+
const tab = entry.indexOf(" "), file = entry.slice(tab + 1);
|
|
2433
|
+
if (entry.startsWith("120000 ") || entry.startsWith("160000 ")) throw new Error("Symlink or submodule in advisory review scope: " + file);
|
|
2434
|
+
await storePath(root, file);
|
|
2435
|
+
}
|
|
2436
|
+
const paths = finding.evidence.kind === "missing-path" ? await Promise.all(
|
|
2437
|
+
(finding.evidence.scope?.candidates ?? [finding.evidence.resolvedPath ?? finding.evidence.claimed]).map(async (file) => ({ path: file, exists: await pathExists(root, file) }))
|
|
2438
|
+
) : [];
|
|
2439
|
+
let dirty = status.length > 0;
|
|
2440
|
+
if (finding.evidence.kind === "missing-script") {
|
|
2441
|
+
for (const file of [...finding.evidence.manifestsChecked, ...finding.evidence.scope?.candidates ?? []]) {
|
|
2442
|
+
if (!tracked.includes(file) && await pathExists(root, file)) dirty = true;
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
const fingerprint = digest([scope, tree, lastScopeCommit, paths]);
|
|
2446
|
+
if ((await git2(root, "rev-parse", "HEAD")).trim() !== head) throw new Error("HEAD changed while collecting review evidence.");
|
|
2447
|
+
return {
|
|
2448
|
+
...evidenceSchema2.parse({ head, scope, tree, lastScopeCommit, paths, fingerprint }),
|
|
2449
|
+
dirty,
|
|
2450
|
+
documentDigest: digest(document)
|
|
2451
|
+
};
|
|
2452
|
+
}
|
|
2453
|
+
async function decisionAssessment(root, finding, baselineHead) {
|
|
2454
|
+
const evidence = finding.evidence;
|
|
2455
|
+
if (evidence.kind !== "decision-anchor") return null;
|
|
2456
|
+
const id = findingId(finding);
|
|
2457
|
+
const store = await loadDecisionStore(root);
|
|
2458
|
+
if (store.diagnostics.length) throw new Error("Decision records have unavailable or malformed evidence; review cannot be confirmed.");
|
|
2459
|
+
const record = store.records.find((record2) => record2.id === evidence.decisionId);
|
|
2460
|
+
if (!record || record.version !== 2) return null;
|
|
2461
|
+
const events2 = record.history.filter((event2) => ["accepted", "reaffirmed", "retired"].includes(event2.kind));
|
|
2462
|
+
const event = events2.at(-1);
|
|
2463
|
+
if (!event?.evidence || !event.actor || !event.note || !event.evidence.historyAvailable || event.evidence.localChanges.length) return null;
|
|
2464
|
+
try {
|
|
2465
|
+
await git2(root, "merge-base", "--is-ancestor", baselineHead, event.evidence.headHash);
|
|
2466
|
+
} catch {
|
|
2467
|
+
return null;
|
|
2468
|
+
}
|
|
2469
|
+
const drift = await computeDecisionDrift(root, [record]);
|
|
2470
|
+
if (!drift.historyAvailable) throw new Error("Decision review history is unavailable.");
|
|
2471
|
+
const retired = record.status === "retired" && event.kind === "retired";
|
|
2472
|
+
const current = retired || record.status === "active" && record.approval === "accepted" && drift.freshness?.[record.id] === "current" && !drift.pendingProposals?.[record.id];
|
|
2473
|
+
return {
|
|
2474
|
+
id,
|
|
2475
|
+
status: current ? "current" : "reopened",
|
|
2476
|
+
outcome: event.kind,
|
|
2477
|
+
reviewer: event.actor,
|
|
2478
|
+
note: event.note,
|
|
2479
|
+
reviewedHead: event.evidence.headHash,
|
|
2480
|
+
recordPath: `.mason/decisions/${record.id}.json`,
|
|
2481
|
+
reason: current ? "The existing decision review records acceptance, reaffirmation or retirement covering the original finding." : "Decision content or anchor evidence changed after its review; review the decision again."
|
|
2482
|
+
};
|
|
2483
|
+
}
|
|
2484
|
+
async function assessAdvisory(root, finding, baselineHead) {
|
|
2485
|
+
if ("confidence" in finding) return null;
|
|
2486
|
+
const id = findingId(finding);
|
|
2487
|
+
try {
|
|
2488
|
+
if (finding.evidence.kind === "decision-anchor") return await decisionAssessment(root, finding, baselineHead);
|
|
2489
|
+
const record = await readRecord(root, id);
|
|
2490
|
+
if (!record) return null;
|
|
2491
|
+
const event = record.events.at(-1);
|
|
2492
|
+
const current = await reviewEvidence(root, event.finding);
|
|
2493
|
+
await git2(root, "merge-base", "--is-ancestor", event.evidence.head, current.head);
|
|
2494
|
+
const base = {
|
|
2495
|
+
id,
|
|
2496
|
+
recordPath: recordPath(id),
|
|
2497
|
+
reviewer: event.reviewer,
|
|
2498
|
+
note: event.note,
|
|
2499
|
+
outcome: event.outcome,
|
|
2500
|
+
reviewedHead: event.evidence.head
|
|
2501
|
+
};
|
|
2502
|
+
if (current.dirty || current.fingerprint !== event.evidence.fingerprint || digest(scopes(finding)) !== digest(event.evidence.scope)) return { ...base, status: "reopened", reason: "Relevant document or repository evidence changed after the assessment; prepare a new review." };
|
|
2503
|
+
return { ...base, status: event.outcome === "deferred" ? "deferred" : "current", reason: event.outcome === "deferred" ? "The review was deferred and remains outstanding." : "An explicit assessment covers the current committed scope. This does not approve an engineering decision or certify correctness." };
|
|
2504
|
+
} catch (error) {
|
|
2505
|
+
return { id, status: "unverified", reason: error instanceof Error ? error.message : String(error) };
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
async function advisoryReviewInventory(root) {
|
|
2509
|
+
let names;
|
|
2510
|
+
try {
|
|
2511
|
+
names = await fs10.readdir(await storePath(root, ".mason/reviews/advisories"));
|
|
2512
|
+
} catch (error) {
|
|
2513
|
+
if (error.code === "ENOENT") return [];
|
|
2514
|
+
throw error;
|
|
2515
|
+
}
|
|
2516
|
+
if (names.length > 5e3) throw new Error("Advisory review inventory exceeds 5,000 records.");
|
|
2517
|
+
return Promise.all(names.filter((name) => name.endsWith(".json")).sort().map(async (name) => [name, await readAuditInput(root, ".mason/reviews/advisories/" + name)]));
|
|
2518
|
+
}
|
|
2519
|
+
var execute2, git2, hashSchema, commitSchema2, evidenceSchema2, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
|
|
2520
|
+
var init_advisory_review = __esm({
|
|
2521
|
+
"src/audit/advisory-review.ts"() {
|
|
2522
|
+
"use strict";
|
|
2523
|
+
init_findings();
|
|
2524
|
+
init_deps_changed();
|
|
2525
|
+
init_inputs();
|
|
2526
|
+
init_scope();
|
|
2527
|
+
init_storage();
|
|
2528
|
+
init_paths();
|
|
2529
|
+
init_store();
|
|
2530
|
+
init_decisions();
|
|
2531
|
+
init_drift2();
|
|
2532
|
+
execute2 = promisify8(execFile8);
|
|
2533
|
+
git2 = async (root, ...args) => (await execute2("git", args, { cwd: root, timeout: 1e4, maxBuffer: 8 * 1024 * 1024 })).stdout;
|
|
2534
|
+
hashSchema = z6.string().regex(/^[a-f0-9]{64}$/);
|
|
2535
|
+
commitSchema2 = z6.string().regex(/^[a-f0-9]{40,64}$/);
|
|
2536
|
+
evidenceSchema2 = z6.object({
|
|
2537
|
+
head: commitSchema2,
|
|
2538
|
+
fingerprint: hashSchema,
|
|
2539
|
+
scope: z6.array(z6.string()).min(1).max(1e3),
|
|
2540
|
+
tree: z6.string().max(2 * 1024 * 1024),
|
|
2541
|
+
lastScopeCommit: commitSchema2,
|
|
2542
|
+
paths: z6.array(z6.object({ path: z6.string(), exists: z6.boolean() })).max(1e3)
|
|
2543
|
+
});
|
|
2544
|
+
eventSchema2 = z6.object({
|
|
2545
|
+
at: z6.string().datetime(),
|
|
2546
|
+
reviewer: z6.string().trim().min(1).max(200),
|
|
2547
|
+
note: z6.string().trim().min(1).max(2500),
|
|
2548
|
+
outcome: z6.enum(["addressed", "inapplicable", "deferred"]),
|
|
2549
|
+
finding: advisorySchema.refine((finding) => finding.evidence.kind !== "decision-anchor", "Decision findings use decision reviews"),
|
|
2550
|
+
evidence: evidenceSchema2
|
|
2551
|
+
});
|
|
2552
|
+
recordSchema = z6.object({ version: z6.literal(1), id: hashSchema, events: z6.array(eventSchema2).min(1).max(200), digest: hashSchema });
|
|
2553
|
+
reviewSummarySchema = z6.object({
|
|
2554
|
+
id: hashSchema,
|
|
2555
|
+
status: z6.enum(["current", "reopened", "deferred", "unverified"]),
|
|
2556
|
+
reason: z6.string(),
|
|
2557
|
+
recordPath: z6.string().optional(),
|
|
2558
|
+
reviewer: z6.string().optional(),
|
|
2559
|
+
note: z6.string().optional(),
|
|
2560
|
+
outcome: z6.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired"]).optional(),
|
|
2561
|
+
reviewedHead: z6.string().optional()
|
|
2562
|
+
});
|
|
2563
|
+
advisoryReviewRequest = z6.object({
|
|
2564
|
+
baselinePath: z6.string().min(1),
|
|
2565
|
+
findingId: hashSchema,
|
|
2566
|
+
action: z6.enum(["prepare", "addressed", "inapplicable", "deferred"]).default("prepare"),
|
|
2567
|
+
reviewer: eventSchema2.shape.reviewer.optional(),
|
|
2568
|
+
note: eventSchema2.shape.note.optional(),
|
|
2569
|
+
reviewToken: hashSchema.optional()
|
|
2570
|
+
});
|
|
2571
|
+
recordPath = (id) => `.mason/reviews/advisories/${hashSchema.parse(id)}.json`;
|
|
2572
|
+
}
|
|
2573
|
+
});
|
|
2574
|
+
|
|
2059
2575
|
// src/audit/audit.ts
|
|
2060
|
-
import
|
|
2061
|
-
import
|
|
2576
|
+
import fs11 from "fs/promises";
|
|
2577
|
+
import path18 from "path";
|
|
2062
2578
|
async function computeAudit(rootDir, options = {}) {
|
|
2063
|
-
const resolvedRoot =
|
|
2579
|
+
const resolvedRoot = path18.resolve(rootDir);
|
|
2064
2580
|
const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
|
|
2065
2581
|
if (docs.length === 0) return null;
|
|
2066
2582
|
const report = {
|
|
@@ -2071,6 +2587,8 @@ async function computeAudit(rootDir, options = {}) {
|
|
|
2071
2587
|
checksRun: [],
|
|
2072
2588
|
docs: docs.map((d) => ({
|
|
2073
2589
|
path: d.path,
|
|
2590
|
+
scope: d.scope,
|
|
2591
|
+
kind: d.kind,
|
|
2074
2592
|
lastCommit: d.lastCommit,
|
|
2075
2593
|
dirty: d.dirty,
|
|
2076
2594
|
lineCount: d.lineCount
|
|
@@ -2092,7 +2610,7 @@ async function computeAudit(rootDir, options = {}) {
|
|
|
2092
2610
|
}
|
|
2093
2611
|
let decisionsPresent = false;
|
|
2094
2612
|
try {
|
|
2095
|
-
await
|
|
2613
|
+
await fs11.access(path18.join(resolvedRoot, ".mason", "decisions"));
|
|
2096
2614
|
decisionsPresent = true;
|
|
2097
2615
|
} catch {
|
|
2098
2616
|
}
|
|
@@ -2115,6 +2633,8 @@ async function computeAudit(rootDir, options = {}) {
|
|
|
2115
2633
|
report.skippedChecks.push(...skipped);
|
|
2116
2634
|
}
|
|
2117
2635
|
report.clean = report.issues.length === 0;
|
|
2636
|
+
const reviews = (await Promise.all([...report.advisories, ...report.suppressedAdvisories].map((finding) => assessAdvisory(resolvedRoot, finding, headHash)))).filter((review) => review !== null);
|
|
2637
|
+
if (reviews.length) report.advisoryReviews = reviews;
|
|
2118
2638
|
return report;
|
|
2119
2639
|
}
|
|
2120
2640
|
var init_audit = __esm({
|
|
@@ -2125,45 +2645,24 @@ var init_audit = __esm({
|
|
|
2125
2645
|
init_docs();
|
|
2126
2646
|
init_types();
|
|
2127
2647
|
init_checks();
|
|
2648
|
+
init_advisory_review();
|
|
2128
2649
|
}
|
|
2129
2650
|
});
|
|
2130
2651
|
|
|
2131
2652
|
// src/audit/repair.ts
|
|
2132
|
-
import
|
|
2133
|
-
import
|
|
2134
|
-
import {
|
|
2135
|
-
import { z as
|
|
2136
|
-
function
|
|
2137
|
-
|
|
2138
|
-
let key;
|
|
2139
|
-
switch (e.kind) {
|
|
2140
|
-
case "missing-path":
|
|
2141
|
-
key = e.claimed;
|
|
2142
|
-
break;
|
|
2143
|
-
case "unmentioned-dir":
|
|
2144
|
-
key = e.dir;
|
|
2145
|
-
break;
|
|
2146
|
-
case "count-mismatch":
|
|
2147
|
-
key = [e.unit.replace(/s$/, ""), e.countedFrom];
|
|
2148
|
-
break;
|
|
2149
|
-
case "missing-script":
|
|
2150
|
-
key = e.scriptName;
|
|
2151
|
-
break;
|
|
2152
|
-
case "doc-behind-manifests":
|
|
2153
|
-
key = null;
|
|
2154
|
-
break;
|
|
2155
|
-
case "decision-anchor":
|
|
2156
|
-
key = [e.decisionId, e.provenance?.revision, e.provenance?.approval];
|
|
2157
|
-
break;
|
|
2158
|
-
}
|
|
2159
|
-
return digest([finding.type, finding.anchor.doc, key]);
|
|
2653
|
+
import fs12 from "fs/promises";
|
|
2654
|
+
import path19 from "path";
|
|
2655
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2656
|
+
import { z as z7 } from "zod";
|
|
2657
|
+
function recheckable(finding) {
|
|
2658
|
+
return !("confidence" in finding) && finding.resolution === "recheck" && ["deleted-reference", "dead-command", "new-module"].includes(finding.type) && ["missing-path", "missing-script", "unmentioned-dir"].includes(finding.evidence.kind);
|
|
2160
2659
|
}
|
|
2161
2660
|
function allFindings(report) {
|
|
2162
2661
|
return [...report.issues, ...report.advisories, ...report.suppressedAdvisories ?? []];
|
|
2163
2662
|
}
|
|
2164
2663
|
async function docState(root) {
|
|
2165
2664
|
const docs = [];
|
|
2166
|
-
for (const doc of
|
|
2665
|
+
for (const doc of await discoverDocPaths(root)) {
|
|
2167
2666
|
try {
|
|
2168
2667
|
const content2 = await readBoundedFile(await storePath(root, doc), 10 * 1024 * 1024);
|
|
2169
2668
|
if (content2 === null) throw new Error("Context file is not regular or exceeds 10 MiB: " + doc);
|
|
@@ -2185,8 +2684,8 @@ async function stableAudit(root, checks, options = {}) {
|
|
|
2185
2684
|
return report;
|
|
2186
2685
|
}
|
|
2187
2686
|
async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
|
|
2188
|
-
const root = await
|
|
2189
|
-
const selected =
|
|
2687
|
+
const root = await fs12.realpath(rootDir);
|
|
2688
|
+
const selected = z7.array(checkSchema).nonempty().parse(checks);
|
|
2190
2689
|
const report = await stableAudit(root, selected, options);
|
|
2191
2690
|
if (!report) throw new Error("No context files found to prepare a repair.");
|
|
2192
2691
|
if (!report.gitAvailable) throw new Error("Readable Git history is required to prepare a repair.");
|
|
@@ -2197,27 +2696,33 @@ async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
|
|
|
2197
2696
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2198
2697
|
report: storedReport
|
|
2199
2698
|
};
|
|
2200
|
-
const baselinePath = ".mason/reports/repairs/" +
|
|
2699
|
+
const baselinePath = ".mason/reports/repairs/" + randomUUID2() + ".json";
|
|
2201
2700
|
await writeStoreJson(root, baselinePath, { ...payload, digest: digest(payload) });
|
|
2202
2701
|
return { version: 1, action: "prepare", baselinePath, report };
|
|
2203
2702
|
}
|
|
2204
|
-
async function
|
|
2205
|
-
const root = await
|
|
2206
|
-
const declaredRoot =
|
|
2207
|
-
const relative =
|
|
2703
|
+
async function loadRepairBaseline(rootDir, baselinePath) {
|
|
2704
|
+
const root = await fs12.realpath(rootDir);
|
|
2705
|
+
const declaredRoot = path19.resolve(rootDir);
|
|
2706
|
+
const relative = path19.isAbsolute(baselinePath) ? path19.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
|
|
2208
2707
|
const stored = baselineSchema.parse(await readStoreJson(root, relative));
|
|
2209
2708
|
const { digest: savedDigest, ...payload } = stored;
|
|
2210
2709
|
if (digest(payload) !== savedDigest) throw new Error("Repair baseline was modified; use the original baseline.");
|
|
2211
2710
|
if (stored.report.root !== root) throw new Error("Repair baseline belongs to a different repository.");
|
|
2212
2711
|
const original = stored.report;
|
|
2712
|
+
return { root, relative, original };
|
|
2713
|
+
}
|
|
2714
|
+
async function verifyRepair(rootDir, baselinePath, options = {}) {
|
|
2715
|
+
const { root, relative, original } = await loadRepairBaseline(rootDir, baselinePath);
|
|
2213
2716
|
const diagnostics = [];
|
|
2717
|
+
let reviewDigest;
|
|
2214
2718
|
let current = null;
|
|
2215
2719
|
try {
|
|
2720
|
+
reviewDigest = digest(await advisoryReviewInventory(root));
|
|
2216
2721
|
current = await stableAudit(root, original.checksRun, options);
|
|
2217
2722
|
if (!current) diagnostics.push("No context files remain available to audit.");
|
|
2218
2723
|
else if (!current.gitAvailable) diagnostics.push("Git history is unavailable.");
|
|
2219
2724
|
for (const doc of original.docs) {
|
|
2220
|
-
if (!original.
|
|
2725
|
+
if (!allFindings(original).some((f) => f.anchor.doc === doc.path) || !current?.docs.some((d) => d.path === doc.path)) continue;
|
|
2221
2726
|
const content2 = await readBoundedFile(await storePath(root, doc.path), 10 * 1024 * 1024);
|
|
2222
2727
|
if (content2 === null || !content2.trim()) {
|
|
2223
2728
|
diagnostics.push("Original context file " + doc.path + " is empty or unreadable; losing its claims does not verify a repair.");
|
|
@@ -2234,21 +2739,27 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
|
|
|
2234
2739
|
const originalIds = new Set(originalFindings.map(findingId));
|
|
2235
2740
|
const missingDocs = original.docs.filter((doc) => !current?.docs.some((d) => d.path === doc.path));
|
|
2236
2741
|
for (const doc of missingDocs) diagnostics.push("Original context file " + doc.path + " is unavailable; removing it does not verify a repair.");
|
|
2237
|
-
const findings = originalFindings.map((finding) => {
|
|
2742
|
+
const findings = await Promise.all(originalFindings.map(async (finding) => {
|
|
2238
2743
|
const id = findingId(finding);
|
|
2239
2744
|
const now = currentById.get(id);
|
|
2240
2745
|
const base = { id, original: finding, ...now ? { current: now } : {} };
|
|
2241
2746
|
if (diagnostics.length || !current) {
|
|
2242
2747
|
return { ...base, status: "unverified", reason: "The original audit scope could not be verified. See diagnostics." };
|
|
2243
2748
|
}
|
|
2244
|
-
if ("confidence" in finding
|
|
2749
|
+
if (now && ("confidence" in finding || "confidence" in now)) {
|
|
2245
2750
|
return { ...base, status: "unresolved", reason: "The original check still reports this claim." };
|
|
2246
2751
|
}
|
|
2247
2752
|
const skipped = current.skippedChecks.filter((s) => s.check === finding.type && (!s.doc || s.doc === finding.anchor.doc));
|
|
2248
2753
|
if (!current.checksRun?.includes(finding.type) || skipped.length) {
|
|
2249
2754
|
return { ...base, status: "unverified", reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run." };
|
|
2250
2755
|
}
|
|
2251
|
-
|
|
2756
|
+
const review = await assessAdvisory(root, now ?? finding, original.headHash);
|
|
2757
|
+
if (review) {
|
|
2758
|
+
if (review.status === "unverified") return { ...base, review, status: "unverified", reason: review.reason };
|
|
2759
|
+
if (review.status === "current") return { ...base, review, status: "resolved", reason: review.reason };
|
|
2760
|
+
return { ...base, review, status: "review-required", reason: review.reason };
|
|
2761
|
+
}
|
|
2762
|
+
if (!("confidence" in finding) && (!recheckable(finding) || now)) {
|
|
2252
2763
|
return {
|
|
2253
2764
|
...base,
|
|
2254
2765
|
status: "review-required",
|
|
@@ -2256,11 +2767,27 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
|
|
|
2256
2767
|
};
|
|
2257
2768
|
}
|
|
2258
2769
|
return { ...base, status: "resolved", reason: "The original check ran and no longer reports this claim. Inspect the edit for semantic correctness." };
|
|
2259
|
-
});
|
|
2770
|
+
}));
|
|
2771
|
+
try {
|
|
2772
|
+
if (reviewDigest !== digest(await advisoryReviewInventory(root)) || current?.headHash !== await getCurrentGitHash(root)) {
|
|
2773
|
+
diagnostics.push("HEAD or advisory review records changed during verification; repeat against stable evidence.");
|
|
2774
|
+
for (const finding of findings) {
|
|
2775
|
+
finding.status = "unverified";
|
|
2776
|
+
finding.reason = diagnostics.at(-1);
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
} catch (error) {
|
|
2780
|
+
diagnostics.push(String(error));
|
|
2781
|
+
for (const finding of findings) {
|
|
2782
|
+
finding.status = "unverified";
|
|
2783
|
+
finding.reason = "Advisory review evidence became unavailable during verification.";
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2260
2786
|
const newFindings = [...currentById].filter(([id]) => !originalIds.has(id)).map(([, f]) => f);
|
|
2787
|
+
const reviewedIds = new Set(current?.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
|
|
2261
2788
|
const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
|
|
2262
2789
|
for (const f of findings) counts[f.status]++;
|
|
2263
|
-
const incomplete = diagnostics.length > 0 || counts.unverified > 0 || counts["review-required"] > 0 || (current?.skippedChecks.length ?? 0) > 0 || newFindings.some((f) => !("confidence" in f));
|
|
2790
|
+
const incomplete = diagnostics.length > 0 || counts.unverified > 0 || counts["review-required"] > 0 || (current?.skippedChecks.length ?? 0) > 0 || newFindings.some((f) => !("confidence" in f) && !reviewedIds.has(findingId(f)));
|
|
2264
2791
|
const issuesRemain = counts.unresolved > 0 || newFindings.some((f) => "confidence" in f);
|
|
2265
2792
|
return {
|
|
2266
2793
|
version: 1,
|
|
@@ -2274,10 +2801,10 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
|
|
|
2274
2801
|
diagnostics,
|
|
2275
2802
|
currentAudit: current,
|
|
2276
2803
|
counts,
|
|
2277
|
-
scope: "Original audit checks over current context files and repository evidence. Resolved means no longer detected by
|
|
2804
|
+
scope: "Original audit checks over current context files and repository evidence. Resolved means no longer detected by the check or covered by an explicit assessment of current evidence; inspect each finding's reason and review. This is not a certification of documentation or application correctness."
|
|
2278
2805
|
};
|
|
2279
2806
|
}
|
|
2280
|
-
var
|
|
2807
|
+
var count2, reportSchema, baselineSchema, repairFindingSchema;
|
|
2281
2808
|
var init_repair = __esm({
|
|
2282
2809
|
"src/audit/repair.ts"() {
|
|
2283
2810
|
"use strict";
|
|
@@ -2289,95 +2816,46 @@ var init_repair = __esm({
|
|
|
2289
2816
|
init_files();
|
|
2290
2817
|
init_paths();
|
|
2291
2818
|
init_types();
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
renamedTo: z4.string().nullable(),
|
|
2301
|
-
deletedInCommit: commitSchema.nullable(),
|
|
2302
|
-
everTracked: z4.boolean(),
|
|
2303
|
-
parentDirExists: z4.boolean()
|
|
2304
|
-
}),
|
|
2305
|
-
z4.object({
|
|
2306
|
-
kind: z4.literal("unmentioned-dir"),
|
|
2307
|
-
dir: z4.string(),
|
|
2308
|
-
sourceFileCount: count,
|
|
2309
|
-
firstCommit: commitSchema.nullable(),
|
|
2310
|
-
checkedDocs: z4.array(z4.string())
|
|
2311
|
-
}),
|
|
2312
|
-
z4.object({
|
|
2313
|
-
kind: z4.literal("count-mismatch"),
|
|
2314
|
-
claimed: count,
|
|
2315
|
-
actual: count,
|
|
2316
|
-
unit: z4.string(),
|
|
2317
|
-
countedFrom: z4.string(),
|
|
2318
|
-
members: z4.array(z4.string())
|
|
2319
|
-
}),
|
|
2320
|
-
z4.object({
|
|
2321
|
-
kind: z4.literal("missing-script"),
|
|
2322
|
-
scriptName: z4.string(),
|
|
2323
|
-
invocation: z4.string(),
|
|
2324
|
-
manifestsChecked: z4.array(z4.string()),
|
|
2325
|
-
availableScripts: z4.array(z4.string())
|
|
2326
|
-
}),
|
|
2327
|
-
z4.object({
|
|
2328
|
-
kind: z4.literal("doc-behind-manifests"),
|
|
2329
|
-
docLastCommit: commitSchema,
|
|
2330
|
-
manifestCommits: z4.array(commitSchema.extend({ files: z4.array(z4.string()) })),
|
|
2331
|
-
totalCommits: count
|
|
2332
|
-
}),
|
|
2333
|
-
z4.object({
|
|
2334
|
-
kind: z4.literal("decision-anchor"),
|
|
2335
|
-
decisionId: z4.string(),
|
|
2336
|
-
title: z4.string(),
|
|
2337
|
-
changedFiles: z4.array(z4.string()),
|
|
2338
|
-
refreshedHash: z4.string(),
|
|
2339
|
-
provenance: z4.object({}).passthrough().optional()
|
|
2340
|
-
})
|
|
2341
|
-
]);
|
|
2342
|
-
findingSchema = z4.object({ message: z4.string(), anchor: anchorSchema, evidence: evidenceSchema });
|
|
2343
|
-
issueSchema = findingSchema.extend({
|
|
2344
|
-
type: z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
|
|
2345
|
-
confidence: z4.enum(["certain", "likely"])
|
|
2346
|
-
});
|
|
2347
|
-
advisorySchema = findingSchema.extend({ type: z4.enum(["deps-changed", "decision-anchor-drift"]) });
|
|
2348
|
-
checkResultSchema = z4.object({
|
|
2349
|
-
issues: z4.array(issueSchema),
|
|
2350
|
-
advisories: z4.array(advisorySchema),
|
|
2351
|
-
suppressedAdvisories: z4.array(advisorySchema).optional(),
|
|
2352
|
-
skipped: z4.array(z4.object({ check: z4.string(), reason: z4.string(), doc: z4.string().optional() }))
|
|
2353
|
-
});
|
|
2354
|
-
reportSchema = z4.object({
|
|
2355
|
-
version: z4.literal(1),
|
|
2356
|
-
root: z4.string(),
|
|
2357
|
-
gitAvailable: z4.literal(true),
|
|
2819
|
+
init_findings();
|
|
2820
|
+
init_advisory_review();
|
|
2821
|
+
init_findings();
|
|
2822
|
+
count2 = z7.number().int().nonnegative();
|
|
2823
|
+
reportSchema = z7.object({
|
|
2824
|
+
version: z7.literal(1),
|
|
2825
|
+
root: z7.string(),
|
|
2826
|
+
gitAvailable: z7.literal(true),
|
|
2358
2827
|
headHash: commitSchema.shape.hash,
|
|
2359
|
-
checksRun:
|
|
2360
|
-
docs:
|
|
2361
|
-
path:
|
|
2828
|
+
checksRun: z7.array(checkSchema).nonempty(),
|
|
2829
|
+
docs: z7.array(z7.object({
|
|
2830
|
+
path: z7.string().refine((value) => normalizeRepoPath(value) === value),
|
|
2831
|
+
scope: z7.string().optional(),
|
|
2832
|
+
kind: z7.enum(["instructions", "readme"]).optional(),
|
|
2362
2833
|
lastCommit: commitSchema.nullable(),
|
|
2363
|
-
dirty:
|
|
2364
|
-
lineCount:
|
|
2834
|
+
dirty: z7.boolean(),
|
|
2835
|
+
lineCount: count2
|
|
2365
2836
|
})).nonempty(),
|
|
2366
|
-
decisionsChecked:
|
|
2367
|
-
clean:
|
|
2368
|
-
issues:
|
|
2369
|
-
advisories:
|
|
2370
|
-
suppressedAdvisories:
|
|
2371
|
-
skippedChecks:
|
|
2837
|
+
decisionsChecked: z7.boolean(),
|
|
2838
|
+
clean: z7.boolean(),
|
|
2839
|
+
issues: z7.array(issueSchema),
|
|
2840
|
+
advisories: z7.array(advisorySchema),
|
|
2841
|
+
suppressedAdvisories: z7.array(advisorySchema).optional(),
|
|
2842
|
+
skippedChecks: z7.array(z7.object({ check: z7.string(), reason: z7.string(), doc: z7.string().optional() }))
|
|
2372
2843
|
});
|
|
2373
|
-
baselineSchema =
|
|
2374
|
-
kind:
|
|
2375
|
-
version:
|
|
2376
|
-
createdAt:
|
|
2844
|
+
baselineSchema = z7.object({
|
|
2845
|
+
kind: z7.literal("mason-audit-repair"),
|
|
2846
|
+
version: z7.literal(1),
|
|
2847
|
+
createdAt: z7.string().datetime(),
|
|
2377
2848
|
report: reportSchema,
|
|
2378
|
-
digest:
|
|
2849
|
+
digest: z7.string().regex(/^[a-f0-9]{64}$/)
|
|
2850
|
+
});
|
|
2851
|
+
repairFindingSchema = z7.object({
|
|
2852
|
+
id: z7.string(),
|
|
2853
|
+
original: z7.union([issueSchema, advisorySchema]),
|
|
2854
|
+
status: z7.enum(["resolved", "unresolved", "review-required", "unverified"]),
|
|
2855
|
+
reason: z7.string(),
|
|
2856
|
+
current: z7.union([issueSchema, advisorySchema]).optional(),
|
|
2857
|
+
review: reviewSummarySchema.optional()
|
|
2379
2858
|
});
|
|
2380
|
-
digest = (value) => createHash2("sha256").update(JSON.stringify(value)).digest("hex");
|
|
2381
2859
|
}
|
|
2382
2860
|
});
|
|
2383
2861
|
|
|
@@ -2385,102 +2863,94 @@ var init_repair = __esm({
|
|
|
2385
2863
|
var evidence_exports = {};
|
|
2386
2864
|
__export(evidence_exports, {
|
|
2387
2865
|
checkCache: () => checkCache,
|
|
2388
|
-
git: () =>
|
|
2866
|
+
git: () => git3,
|
|
2389
2867
|
hash: () => hash,
|
|
2390
2868
|
readInputs: () => readInputs,
|
|
2391
2869
|
workspace: () => workspace
|
|
2392
2870
|
});
|
|
2393
|
-
import
|
|
2394
|
-
import
|
|
2395
|
-
import { createHash as createHash3 } from "crypto";
|
|
2396
|
-
import { execFile as
|
|
2397
|
-
import { promisify as
|
|
2398
|
-
import
|
|
2399
|
-
import { z as
|
|
2400
|
-
async function
|
|
2871
|
+
import fs13 from "fs/promises";
|
|
2872
|
+
import path20 from "path";
|
|
2873
|
+
import { createHash as createHash3 } from "crypto";
|
|
2874
|
+
import { execFile as execFile9 } from "child_process";
|
|
2875
|
+
import { promisify as promisify9 } from "util";
|
|
2876
|
+
import fg5 from "fast-glob";
|
|
2877
|
+
import { z as z8 } from "zod";
|
|
2878
|
+
async function git3(root, ...args) {
|
|
2401
2879
|
return (await exec7("git", args, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout;
|
|
2402
2880
|
}
|
|
2403
2881
|
async function workspace(dir) {
|
|
2404
|
-
const root = await
|
|
2405
|
-
const gitDir = await
|
|
2882
|
+
const root = await fs13.realpath((await git3(dir, "rev-parse", "--show-toplevel")).trim());
|
|
2883
|
+
const gitDir = await fs13.realpath((await git3(root, "rev-parse", "--absolute-git-dir")).trim());
|
|
2406
2884
|
let branch;
|
|
2407
2885
|
try {
|
|
2408
|
-
branch = (await
|
|
2886
|
+
branch = (await git3(root, "symbolic-ref", "--quiet", "HEAD")).trim();
|
|
2409
2887
|
} catch {
|
|
2410
2888
|
branch = "detached";
|
|
2411
2889
|
}
|
|
2412
2890
|
return { root, gitDir, branch, directory: ".mason/reports/automation/" + hash([root, gitDir, branch]).slice(0, 24) };
|
|
2413
2891
|
}
|
|
2414
2892
|
async function readInputs(root) {
|
|
2893
|
+
const docPaths = await discoverDocPaths(root);
|
|
2415
2894
|
const [headText, docStatus, shallowPath, replacements] = await Promise.all([
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2895
|
+
git3(root, "rev-parse", "HEAD"),
|
|
2896
|
+
docPaths.length ? git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...docPaths.map((p) => `:(literal)${p}`)) : "",
|
|
2897
|
+
git3(root, "rev-parse", "--git-path", "shallow"),
|
|
2898
|
+
git3(root, "for-each-ref", "--format=%(refname) %(objectname)", "refs/replace")
|
|
2420
2899
|
]);
|
|
2421
2900
|
const head = headText.trim();
|
|
2422
2901
|
let shallow = null;
|
|
2423
2902
|
try {
|
|
2424
|
-
shallow = await
|
|
2903
|
+
shallow = await fs13.readFile(path20.resolve(root, shallowPath.trim()), "utf8");
|
|
2425
2904
|
} catch (error) {
|
|
2426
2905
|
if (error.code !== "ENOENT") throw error;
|
|
2427
2906
|
}
|
|
2428
2907
|
const docs = {};
|
|
2429
2908
|
const docContents = [];
|
|
2430
2909
|
const claims = [];
|
|
2431
|
-
for (const file of
|
|
2910
|
+
for (const file of docPaths) {
|
|
2432
2911
|
const text2 = await content(root, file);
|
|
2433
2912
|
docs[file] = text2 === null ? null : hash(text2);
|
|
2434
2913
|
docContents.push([file, text2]);
|
|
2435
2914
|
for (const claim of text2 ? extractClaims(text2).paths : []) {
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
const combinedDocs = docContents.map(([, text2]) => text2 ?? "").join("\n");
|
|
2442
|
-
const countClaims = docContents.flatMap(([, text2]) => text2 ? extractClaims(text2).counts : []);
|
|
2443
|
-
const commandClaims = docContents.flatMap(([, text2]) => text2 ? extractClaims(text2).commands : []);
|
|
2444
|
-
const rootPackage = await content(root, "package.json");
|
|
2445
|
-
let rootScripts = null;
|
|
2446
|
-
if (rootPackage !== null) {
|
|
2447
|
-
try {
|
|
2448
|
-
const pkg = JSON.parse(rootPackage);
|
|
2449
|
-
rootScripts = pkg?.scripts && typeof pkg.scripts === "object" ? Object.keys(pkg.scripts) : [];
|
|
2450
|
-
} catch {
|
|
2915
|
+
const scope = pathClaimScope(file, claim);
|
|
2916
|
+
for (const candidate of scope?.candidates ?? []) {
|
|
2917
|
+
if (optionalMasonPath(candidate)) continue;
|
|
2918
|
+
claims.push([candidate, await pathExists(root, candidate), await pathExists(root, path20.posix.dirname(candidate))]);
|
|
2919
|
+
}
|
|
2451
2920
|
}
|
|
2452
2921
|
}
|
|
2453
|
-
const
|
|
2922
|
+
const parsedDocs = docContents.map(([file, text2]) => ({ path: file, content: text2 ?? "", claims: extractClaims(text2 ?? "") }));
|
|
2923
|
+
const combinedDocs = moduleDocumentation(parsedDocs);
|
|
2924
|
+
const countClaims = parsedDocs.flatMap((doc) => doc.claims.counts.map((claim) => ({ doc: doc.path, claim })));
|
|
2454
2925
|
const decisionDirectory = await storePath(root, ".mason/decisions");
|
|
2455
|
-
const decisionPresence = await
|
|
2926
|
+
const decisionPresence = await fs13.lstat(decisionDirectory).then((stat) => stat.isDirectory() ? "directory" : "file", (error) => {
|
|
2456
2927
|
if (error.code === "ENOENT") return "absent";
|
|
2457
2928
|
throw error;
|
|
2458
2929
|
});
|
|
2459
|
-
const [modules, counts,
|
|
2930
|
+
const [modules, counts, commands, decisionFiles] = await Promise.all([
|
|
2460
2931
|
combinedDocs ? moduleCandidates(root, combinedDocs) : [],
|
|
2461
|
-
Promise.all(countClaims.map((claim) =>
|
|
2462
|
-
|
|
2463
|
-
|
|
2932
|
+
Promise.all(countClaims.map(({ doc, claim }) => resolveDocCountSource(root, doc, claim))),
|
|
2933
|
+
commandInputs(root, parsedDocs),
|
|
2934
|
+
fg5(".mason/decisions/*.json", { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false })
|
|
2464
2935
|
]);
|
|
2465
|
-
const packages = [["package.json", rootPackage], ...await Promise.all(manifests.sort().map(async (file) => [file, await content(root, file)]))];
|
|
2466
2936
|
const decisions = await Promise.all(decisionFiles.sort().map(async (file) => {
|
|
2467
2937
|
await storePath(root, file);
|
|
2468
2938
|
return [file, await content(root, file)];
|
|
2469
2939
|
}));
|
|
2470
2940
|
const [status, index2] = decisions.length ? await Promise.all([
|
|
2471
|
-
|
|
2472
|
-
|
|
2941
|
+
git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".", ":(exclude).mason/reports"),
|
|
2942
|
+
git3(root, "ls-files", "--stage", "-z", "--", ".", ":(exclude).mason/reports")
|
|
2473
2943
|
]) : ["", ""];
|
|
2474
|
-
const common = [
|
|
2944
|
+
const common = [4, engineVersion, head, shallow, replacements, docContents];
|
|
2475
2945
|
const keys = {
|
|
2476
2946
|
"deleted-reference": hash([common, claims, docStatus]),
|
|
2477
2947
|
"new-module": hash([common, modules]),
|
|
2478
2948
|
"stale-count": hash([common, counts]),
|
|
2479
|
-
"dead-command": hash([common,
|
|
2949
|
+
"dead-command": hash([common, commands]),
|
|
2480
2950
|
"deps-changed": hash([common, docStatus]),
|
|
2481
2951
|
"decision-anchor-drift": hash([common, decisionPresence, decisions, status, index2])
|
|
2482
2952
|
};
|
|
2483
|
-
return { fingerprint: hash(keys), head, docs, keys };
|
|
2953
|
+
return { fingerprint: hash([keys, await advisoryReviewInventory(root)]), head, docs, keys };
|
|
2484
2954
|
}
|
|
2485
2955
|
function checkCache(raw, inputs) {
|
|
2486
2956
|
let entries = {};
|
|
@@ -2507,11 +2977,12 @@ function checkCache(raw, inputs) {
|
|
|
2507
2977
|
return { version: 1, entries: canonical, digest: hash(canonical) };
|
|
2508
2978
|
} };
|
|
2509
2979
|
}
|
|
2510
|
-
var exec7, engineVersion, hash, content,
|
|
2980
|
+
var exec7, engineVersion, hash, content, cacheSchema;
|
|
2511
2981
|
var init_evidence = __esm({
|
|
2512
2982
|
"src/automation/evidence.ts"() {
|
|
2513
2983
|
"use strict";
|
|
2514
2984
|
init_docs();
|
|
2985
|
+
init_scope();
|
|
2515
2986
|
init_claims();
|
|
2516
2987
|
init_repair();
|
|
2517
2988
|
init_checks();
|
|
@@ -2520,114 +2991,201 @@ var init_evidence = __esm({
|
|
|
2520
2991
|
init_stale_count();
|
|
2521
2992
|
init_dead_command();
|
|
2522
2993
|
init_storage();
|
|
2523
|
-
|
|
2524
|
-
|
|
2994
|
+
init_advisory_review();
|
|
2995
|
+
exec7 = promisify9(execFile9);
|
|
2996
|
+
engineVersion = true ? "0.17.0" : "development";
|
|
2525
2997
|
hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
|
|
2526
2998
|
content = readAuditInput;
|
|
2527
|
-
|
|
2528
|
-
cacheSchema = z5.object({ version: z5.literal(1), entries: z5.record(z5.object({ key: z5.string(), result: checkResultSchema })), digest: z5.string() });
|
|
2999
|
+
cacheSchema = z8.object({ version: z8.literal(1), entries: z8.record(z8.object({ key: z8.string(), result: checkResultSchema })), digest: z8.string() });
|
|
2529
3000
|
}
|
|
2530
3001
|
});
|
|
2531
3002
|
|
|
2532
|
-
// src/automation/
|
|
2533
|
-
import fs12 from "fs/promises";
|
|
3003
|
+
// src/automation/execution.ts
|
|
2534
3004
|
import os2 from "os";
|
|
2535
|
-
import
|
|
2536
|
-
|
|
3005
|
+
import fs14 from "fs/promises";
|
|
3006
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
3007
|
+
import { z as z9 } from "zod";
|
|
3008
|
+
function parseExecution(raw) {
|
|
2537
3009
|
try {
|
|
2538
|
-
return
|
|
3010
|
+
return executionSchema.parse(raw);
|
|
2539
3011
|
} catch (error) {
|
|
2540
|
-
throw new Error("Invalid automation
|
|
3012
|
+
throw new Error("Invalid automation execution store; receipt history was retained.", { cause: error });
|
|
2541
3013
|
}
|
|
2542
3014
|
}
|
|
2543
|
-
|
|
2544
|
-
const
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
3015
|
+
function automationFailure(error) {
|
|
3016
|
+
const recorded = failureSchema.safeParse(error?.failure);
|
|
3017
|
+
if (recorded.success) return recorded.data;
|
|
3018
|
+
const message2 = (error instanceof Error ? error.message : String(error)).replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 700);
|
|
3019
|
+
const codes = /* @__PURE__ */ new Set();
|
|
3020
|
+
let cause = error;
|
|
3021
|
+
for (let i = 0; cause && i < 8; i++) {
|
|
3022
|
+
codes.add(String(cause.code));
|
|
3023
|
+
cause = cause.cause;
|
|
3024
|
+
}
|
|
3025
|
+
const code = codes.has("ENOSPC") || codes.has("EDQUOT") ? "storage-full" : /changed during|changed while|changed between/.test(message2) ? "inputs-changed" : /Automation is busy/.test(message2) ? "busy" : /not a git repository|unknown revision|bad revision|different history|unreachable/.test(message2) ? "history-unavailable" : error instanceof z9.ZodError || error instanceof SyntaxError || /Hook input|Expected one command|Unknown automation command|--host/.test(message2) ? "invalid-input" : /store|baseline|modified|symbolic link|Symlink|automation state|state belongs/.test(message2) ? "invalid-evidence" : [...codes].some((c) => /^E[A-Z]+$/.test(c)) ? "io-error" : "internal";
|
|
3026
|
+
return { code, message: message2, retryable: ["inputs-changed", "storage-full", "busy", "io-error"].includes(code), receiptRecorded: false };
|
|
3027
|
+
}
|
|
3028
|
+
function failureMessage(error) {
|
|
3029
|
+
const failure = automationFailure(error);
|
|
3030
|
+
return `Mason automation unavailable [${failure.code}]; evidence capture/verification was not established. ${failure.message}` + (failure.receiptRecorded ? "" : " No durable failure receipt was recorded.");
|
|
3031
|
+
}
|
|
3032
|
+
async function failureNotifications(root, directory) {
|
|
3033
|
+
const raw = await readStoreJson(root, directory + "/notifications.json");
|
|
3034
|
+
return raw === null ? { version: 1, sessions: {} } : notificationsSchema.parse(raw);
|
|
3035
|
+
}
|
|
3036
|
+
async function hookFailureMessage(dir, host, sessionId, error) {
|
|
3037
|
+
const failure = automationFailure(error);
|
|
3038
|
+
if (!failure.receiptRecorded) return failureMessage(error);
|
|
3039
|
+
try {
|
|
3040
|
+
const ws = await workspace(dir);
|
|
3041
|
+
return await withLock(ws.root, ws.directory + "/notifications", async () => {
|
|
3042
|
+
const notices = await failureNotifications(ws.root, ws.directory);
|
|
3043
|
+
const key = hash([host, sessionId]), signature = hash([failure.code, failure.message]);
|
|
3044
|
+
const previous = notices.sessions[key];
|
|
3045
|
+
const execution = await executionStatus(ws.root, ws.directory);
|
|
3046
|
+
const recovered = previous && execution.attempts.some((a) => a.status === "completed" && a.startedAt > previous.lastAt);
|
|
3047
|
+
const repeat = previous?.signature === signature && !recovered;
|
|
3048
|
+
notices.sessions[key] = {
|
|
3049
|
+
signature,
|
|
3050
|
+
code: failure.code,
|
|
3051
|
+
message: failure.message,
|
|
3052
|
+
count: repeat ? previous.count + 1 : 1,
|
|
3053
|
+
lastAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3054
|
+
};
|
|
3055
|
+
notices.sessions = Object.fromEntries(Object.entries(notices.sessions).sort(([, a], [, b]) => b.lastAt.localeCompare(a.lastAt)).slice(0, 32));
|
|
3056
|
+
await writeStoreJson(ws.root, ws.directory + "/notifications.json", notices);
|
|
3057
|
+
return repeat ? null : failureMessage(error) + " Repeated failures in this session are recorded in mason status --json.";
|
|
3058
|
+
});
|
|
3059
|
+
} catch {
|
|
3060
|
+
return failureMessage(error);
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
async function refreshRunning(root, directory, attempts) {
|
|
3064
|
+
for (const attempt of attempts) {
|
|
3065
|
+
if (attempt.status !== "running") continue;
|
|
3066
|
+
let alive = false;
|
|
2548
3067
|
try {
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
})
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
});
|
|
2558
|
-
throw error;
|
|
3068
|
+
const owner = await readStoreJson(root, attempt.lease ? leasePath(directory, attempt.id) : directory + "/lock");
|
|
3069
|
+
if (attempt.host === os2.hostname() && owner?.pid === attempt.pid && owner.host === attempt.host) {
|
|
3070
|
+
try {
|
|
3071
|
+
process.kill(attempt.pid, 0);
|
|
3072
|
+
alive = true;
|
|
3073
|
+
} catch (error) {
|
|
3074
|
+
alive = error.code === "EPERM";
|
|
3075
|
+
}
|
|
2559
3076
|
}
|
|
3077
|
+
} catch {
|
|
3078
|
+
}
|
|
3079
|
+
if (!alive) attempt.status = "unknown";
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
async function editExecution(root, directory, edit) {
|
|
3083
|
+
return withLock(root, directory + "/execution-lock", async () => {
|
|
3084
|
+
const file = directory + "/execution.json";
|
|
3085
|
+
const raw = await readStoreJson(root, file);
|
|
3086
|
+
const log = raw === null ? { version: 1, attempts: [], discardedAttempts: 0 } : parseExecution(raw);
|
|
3087
|
+
await refreshRunning(root, directory, log.attempts);
|
|
3088
|
+
edit(log);
|
|
3089
|
+
const finished = log.attempts.filter((a) => a.status !== "running");
|
|
3090
|
+
const discard = new Set(finished.slice(0, Math.max(0, finished.length - 32)).map((a) => a.id));
|
|
3091
|
+
log.discardedAttempts += discard.size;
|
|
3092
|
+
log.attempts = log.attempts.filter((a) => !discard.has(a.id));
|
|
3093
|
+
await writeStoreJson(root, file, log);
|
|
3094
|
+
});
|
|
3095
|
+
}
|
|
3096
|
+
async function recordExecution(root, directory, event, run) {
|
|
3097
|
+
const started = performance.now();
|
|
3098
|
+
const attempt = {
|
|
3099
|
+
id: randomUUID3(),
|
|
3100
|
+
event,
|
|
3101
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3102
|
+
pid: process.pid,
|
|
3103
|
+
host: os2.hostname(),
|
|
3104
|
+
status: "running",
|
|
3105
|
+
lease: true
|
|
3106
|
+
};
|
|
3107
|
+
const owner = leasePath(directory, attempt.id);
|
|
3108
|
+
const finish = async (changes) => editExecution(root, directory, (log) => {
|
|
3109
|
+
const current = log.attempts.find((a) => a.id === attempt.id);
|
|
3110
|
+
if (!current) throw new Error("Automation execution receipt disappeared; completion could not be recorded.");
|
|
3111
|
+
Object.assign(current, changes, { finishedAt: (/* @__PURE__ */ new Date()).toISOString(), durationMs: performance.now() - started });
|
|
3112
|
+
});
|
|
3113
|
+
await writeStoreJson(root, owner, { pid: attempt.pid, host: attempt.host });
|
|
3114
|
+
try {
|
|
3115
|
+
await editExecution(root, directory, (log) => {
|
|
3116
|
+
if (log.attempts.filter((a) => a.status === "running").length >= 128) throw new Error("Too many active automation executions; existing receipts were retained.");
|
|
3117
|
+
log.attempts.push(attempt);
|
|
3118
|
+
});
|
|
3119
|
+
try {
|
|
3120
|
+
const result = await run();
|
|
3121
|
+
await finish({ status: "completed", verificationStatus: result.report.status, reportPath: result.report.reportPath });
|
|
3122
|
+
return result;
|
|
2560
3123
|
} catch (error) {
|
|
2561
|
-
|
|
3124
|
+
const failure = automationFailure(error);
|
|
2562
3125
|
try {
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
try {
|
|
2566
|
-
process.kill(owner.pid, 0);
|
|
2567
|
-
} catch (probe) {
|
|
2568
|
-
if (probe.code === "ESRCH") {
|
|
2569
|
-
const reclaim = file + ".reclaim";
|
|
2570
|
-
let guard;
|
|
2571
|
-
try {
|
|
2572
|
-
guard = await fs12.open(reclaim, "wx", 384);
|
|
2573
|
-
const current = JSON.parse(await fs12.readFile(file, "utf8"));
|
|
2574
|
-
if (current.pid === owner.pid && current.host === owner.host) await fs12.unlink(file);
|
|
2575
|
-
} finally {
|
|
2576
|
-
if (guard) {
|
|
2577
|
-
await guard.close();
|
|
2578
|
-
await fs12.rm(reclaim, { force: true });
|
|
2579
|
-
}
|
|
2580
|
-
}
|
|
2581
|
-
}
|
|
2582
|
-
}
|
|
2583
|
-
}
|
|
3126
|
+
await finish({ status: "failed", failure: { ...failure, receiptRecorded: true } });
|
|
3127
|
+
failure.receiptRecorded = true;
|
|
2584
3128
|
} catch {
|
|
2585
3129
|
}
|
|
2586
|
-
|
|
2587
|
-
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
3130
|
+
throw Object.assign(new Error(failure.message, { cause: error }), { failure });
|
|
2588
3131
|
}
|
|
2589
|
-
}
|
|
2590
|
-
try {
|
|
2591
|
-
return await run();
|
|
2592
3132
|
} finally {
|
|
2593
|
-
await
|
|
2594
|
-
|
|
3133
|
+
await fs14.rm(await storePath(root, owner), { force: true }).catch(() => {
|
|
3134
|
+
});
|
|
2595
3135
|
}
|
|
2596
3136
|
}
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
3137
|
+
async function executionStatus(root, directory) {
|
|
3138
|
+
const raw = await readStoreJson(root, directory + "/execution.json");
|
|
3139
|
+
if (raw === null) return { status: "not-observed", attempts: [] };
|
|
3140
|
+
const log = parseExecution(raw);
|
|
3141
|
+
await refreshRunning(root, directory, log.attempts);
|
|
3142
|
+
const completed = [...log.attempts].reverse().find((a) => a.status === "completed");
|
|
3143
|
+
const outstanding = [...log.attempts].reverse().find((a) => ["failed", "unknown"].includes(a.status) && (!completed || (a.finishedAt ?? a.startedAt) >= completed.startedAt));
|
|
3144
|
+
const status = log.attempts.some((a) => a.status === "running") ? "running" : outstanding?.status ?? log.attempts.at(-1)?.status ?? "not-observed";
|
|
3145
|
+
return { status, attempts: log.attempts, discardedAttempts: log.discardedAttempts };
|
|
3146
|
+
}
|
|
3147
|
+
var failureSchema, attemptSchema, executionSchema, notificationsSchema, leasePath;
|
|
3148
|
+
var init_execution = __esm({
|
|
3149
|
+
"src/automation/execution.ts"() {
|
|
2600
3150
|
"use strict";
|
|
3151
|
+
init_store();
|
|
3152
|
+
init_evidence();
|
|
2601
3153
|
init_storage();
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
})),
|
|
2622
|
-
updatedAt: z6.string(),
|
|
2623
|
-
fingerprint: z6.string().nullable(),
|
|
2624
|
-
latest: z6.string().nullable()
|
|
3154
|
+
failureSchema = z9.object({
|
|
3155
|
+
code: z9.enum(["inputs-changed", "storage-full", "busy", "invalid-input", "history-unavailable", "invalid-evidence", "io-error", "internal"]),
|
|
3156
|
+
message: z9.string(),
|
|
3157
|
+
retryable: z9.boolean(),
|
|
3158
|
+
receiptRecorded: z9.boolean()
|
|
3159
|
+
});
|
|
3160
|
+
attemptSchema = z9.object({
|
|
3161
|
+
id: z9.string().regex(/^[a-zA-Z0-9_-]+$/),
|
|
3162
|
+
lease: z9.literal(true).optional(),
|
|
3163
|
+
event: z9.string(),
|
|
3164
|
+
startedAt: z9.string(),
|
|
3165
|
+
finishedAt: z9.string().optional(),
|
|
3166
|
+
durationMs: z9.number().nonnegative().optional(),
|
|
3167
|
+
pid: z9.number().int().positive(),
|
|
3168
|
+
host: z9.string(),
|
|
3169
|
+
status: z9.enum(["running", "completed", "failed", "unknown"]),
|
|
3170
|
+
verificationStatus: z9.string().optional(),
|
|
3171
|
+
reportPath: z9.string().optional(),
|
|
3172
|
+
failure: failureSchema.optional()
|
|
2625
3173
|
});
|
|
3174
|
+
executionSchema = z9.object({ version: z9.literal(1), attempts: z9.array(attemptSchema).max(160), discardedAttempts: z9.number().int().nonnegative().default(0) });
|
|
3175
|
+
notificationsSchema = z9.object({ version: z9.literal(1), sessions: z9.record(z9.object({
|
|
3176
|
+
signature: z9.string(),
|
|
3177
|
+
code: failureSchema.shape.code,
|
|
3178
|
+
message: z9.string(),
|
|
3179
|
+
count: z9.number().int().positive(),
|
|
3180
|
+
lastAt: z9.string()
|
|
3181
|
+
})) });
|
|
3182
|
+
leasePath = (directory, id) => `${directory}/executions/${id}.json`;
|
|
2626
3183
|
}
|
|
2627
3184
|
});
|
|
2628
3185
|
|
|
2629
3186
|
// src/automation/runtime.ts
|
|
2630
3187
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
3188
|
+
import { z as z10 } from "zod";
|
|
2631
3189
|
async function observeReadOnlyTool(dir, event) {
|
|
2632
3190
|
const ws = await workspace(dir);
|
|
2633
3191
|
await withLock(ws.root, ws.directory, async () => {
|
|
@@ -2658,176 +3216,234 @@ function summarize(report) {
|
|
|
2658
3216
|
"Keep original evidence. Address findings relevant to the authorized task; report unrelated findings and unresolved advisories without approving them."
|
|
2659
3217
|
].join("\n");
|
|
2660
3218
|
}
|
|
3219
|
+
async function loadState(ws) {
|
|
3220
|
+
const raw = await readStoreJson(ws.root, ws.directory + "/state.json");
|
|
3221
|
+
const state = raw === null ? {
|
|
3222
|
+
version: 1,
|
|
3223
|
+
root: ws.root,
|
|
3224
|
+
gitDir: ws.gitDir,
|
|
3225
|
+
branch: ws.branch,
|
|
3226
|
+
baselines: [],
|
|
3227
|
+
sessions: {},
|
|
3228
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3229
|
+
fingerprint: null,
|
|
3230
|
+
latest: null
|
|
3231
|
+
} : parseState(raw);
|
|
3232
|
+
if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) {
|
|
3233
|
+
throw new Error("Automation state belongs to another branch or worktree; original evidence was retained.");
|
|
3234
|
+
}
|
|
3235
|
+
return state;
|
|
3236
|
+
}
|
|
3237
|
+
async function sharedAnalysis(ws, state, inputs, startedAt) {
|
|
3238
|
+
const pointer = state.analysis;
|
|
3239
|
+
if (!pointer || pointer.fingerprint !== inputs.fingerprint || pointer.completedAt < startedAt) return null;
|
|
3240
|
+
const stored = sharedSchema.parse(await readStoreJson(ws.root, pointer.path));
|
|
3241
|
+
const a = stored.analysis;
|
|
3242
|
+
if (stored.digest !== hash(a) || a.id !== pointer.id || a.fingerprint !== inputs.fingerprint || a.completedAt !== pointer.completedAt || a.report.root !== ws.root || a.report.branch !== ws.branch || a.report.head !== inputs.head || hash(a.report.baselinePaths) !== hash(state.baselines.map((b) => b.path))) {
|
|
3243
|
+
throw new Error("Invalid shared automation evidence; run a new check.");
|
|
3244
|
+
}
|
|
3245
|
+
if (a.baselineDigest !== await baselineDigest(ws, state)) throw new Error("Repair baseline was modified; use the original baseline.");
|
|
3246
|
+
return a;
|
|
3247
|
+
}
|
|
3248
|
+
async function verifyInputs(ws, inputs) {
|
|
3249
|
+
const [after, currentWs] = await Promise.all([readInputs(ws.root), workspace(ws.root)]);
|
|
3250
|
+
if (after.fingerprint !== inputs.fingerprint || currentWs.directory !== ws.directory) {
|
|
3251
|
+
throw new Error("Repository inputs or branch changed during automation; no current verification was recorded. Retry on a stable checkout.");
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
async function computeAnalysis(ws, state, inputs, event) {
|
|
3255
|
+
const baselines = [...state.baselines];
|
|
3256
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3257
|
+
const empty = {
|
|
3258
|
+
version: 1,
|
|
3259
|
+
status: "unavailable",
|
|
3260
|
+
root: ws.root,
|
|
3261
|
+
branch: ws.branch,
|
|
3262
|
+
head: inputs.head,
|
|
3263
|
+
baselinePaths: [],
|
|
3264
|
+
reportPath: "",
|
|
3265
|
+
findings: [],
|
|
3266
|
+
diagnostics: ["No README.md or agent instruction files were discovered. Documentation capture is unavailable; other Mason tools remain usable."],
|
|
3267
|
+
checks: { ran: [], reused: [], skipped: [] },
|
|
3268
|
+
counts: { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 },
|
|
3269
|
+
capture: "unknown",
|
|
3270
|
+
scope: SCOPE
|
|
3271
|
+
};
|
|
3272
|
+
if (!baselines.length && Object.values(inputs.docs).every((value) => value === null)) return { baselines, report: empty, cache: null };
|
|
3273
|
+
if (ws.branch === "detached" && state.latest) {
|
|
3274
|
+
const previous = await readStoreJson(ws.root, state.latest);
|
|
3275
|
+
if (!previous?.head || !/^[a-f0-9]{40,64}$/.test(previous.head)) throw new Error("The previous detached checkout evidence is unavailable.");
|
|
3276
|
+
try {
|
|
3277
|
+
await git3(ws.root, "merge-base", "--is-ancestor", previous.head, inputs.head);
|
|
3278
|
+
} catch {
|
|
3279
|
+
throw new Error("Detached checkout moved to a different history; original repair evidence was retained. Inspect that baseline explicitly.");
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
let cached = null;
|
|
3283
|
+
const diagnostics = [];
|
|
3284
|
+
try {
|
|
3285
|
+
cached = await readStoreJson(ws.root, ws.directory + "/cache.json");
|
|
3286
|
+
} catch {
|
|
3287
|
+
diagnostics.push("Unreadable automation cache; checks are being recomputed.");
|
|
3288
|
+
}
|
|
3289
|
+
const cache = checkCache(cached, inputs);
|
|
3290
|
+
if (cache.diagnostic) diagnostics.push(cache.diagnostic);
|
|
3291
|
+
const saveBaseline = async () => {
|
|
3292
|
+
if (baselines.length >= 128) throw new Error("128 retained baselines need review; automatic capture stopped without discarding original evidence.");
|
|
3293
|
+
const prepared = await prepareRepair(ws.root, ALL_CHECKS, cache.options);
|
|
3294
|
+
baselines.push({ path: prepared.baselinePath, at: now, event: event.event, fingerprint: inputs.fingerprint });
|
|
3295
|
+
};
|
|
3296
|
+
if (!baselines.length) await saveBaseline();
|
|
3297
|
+
const verifications = [];
|
|
3298
|
+
for (const baseline of baselines) verifications.push(await verifyRepair(ws.root, baseline.path, cache.options));
|
|
3299
|
+
const known = new Set(verifications.flatMap((v) => v.findings.map((f) => f.id)));
|
|
3300
|
+
if (verifications.some((v) => v.newFindings.some((f) => !known.has(findingId(f))))) {
|
|
3301
|
+
await saveBaseline();
|
|
3302
|
+
verifications.push(await verifyRepair(ws.root, baselines.at(-1).path, cache.options));
|
|
3303
|
+
}
|
|
3304
|
+
const merged = /* @__PURE__ */ new Map();
|
|
3305
|
+
for (const verification of verifications) {
|
|
3306
|
+
for (const finding of verification.findings) {
|
|
3307
|
+
const previous = merged.get(finding.id);
|
|
3308
|
+
if (!previous || priority[finding.status] > priority[previous.status]) merged.set(finding.id, finding);
|
|
3309
|
+
}
|
|
3310
|
+
diagnostics.push(...verification.diagnostics);
|
|
3311
|
+
}
|
|
3312
|
+
const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
|
|
3313
|
+
for (const finding of merged.values()) counts[finding.status]++;
|
|
3314
|
+
const report = {
|
|
3315
|
+
version: 1,
|
|
3316
|
+
status: diagnostics.length || verifications.some((v) => v.status === "incomplete") ? "incomplete" : counts.unresolved ? "issues-remain" : "verified",
|
|
3317
|
+
root: ws.root,
|
|
3318
|
+
branch: ws.branch,
|
|
3319
|
+
head: inputs.head,
|
|
3320
|
+
baselinePaths: baselines.map((b) => b.path),
|
|
3321
|
+
reportPath: "",
|
|
3322
|
+
findings: [...merged.values()],
|
|
3323
|
+
diagnostics: [...new Set(diagnostics)],
|
|
3324
|
+
checks: { ran: [...cache.ran], reused: [...cache.reused].filter((name) => !cache.ran.has(name)), skipped: verifications.at(-1).currentAudit?.skippedChecks ?? [] },
|
|
3325
|
+
counts,
|
|
3326
|
+
capture: "unknown",
|
|
3327
|
+
scope: SCOPE
|
|
3328
|
+
};
|
|
3329
|
+
return { baselines, report, cache: cache.ran.size || cached === null || cache.diagnostic ? cache.serialize() : null };
|
|
3330
|
+
}
|
|
3331
|
+
async function analyze(ws, inputs, event, startedAt) {
|
|
3332
|
+
return withLock(ws.root, ws.directory + "/analysis/" + inputs.fingerprint, async () => {
|
|
3333
|
+
const before = await loadState(ws);
|
|
3334
|
+
const shared = await sharedAnalysis(ws, before, inputs, startedAt);
|
|
3335
|
+
if (shared) return { analysis: shared, shared: true };
|
|
3336
|
+
const originalDigest = await baselineDigest(ws, before);
|
|
3337
|
+
const computed = await computeAnalysis(ws, before, inputs, event);
|
|
3338
|
+
await verifyInputs(ws, inputs);
|
|
3339
|
+
if (originalDigest !== await baselineDigest(ws, before)) throw new Error("Repair baseline was modified during automation; original evidence must be inspected.");
|
|
3340
|
+
const id = randomUUID4();
|
|
3341
|
+
const analysis = analysisSchema.parse({
|
|
3342
|
+
id,
|
|
3343
|
+
fingerprint: inputs.fingerprint,
|
|
3344
|
+
completedAt: Date.now(),
|
|
3345
|
+
baselineDigest: await baselineDigest(ws, { ...before, baselines: computed.baselines }),
|
|
3346
|
+
report: computed.report
|
|
3347
|
+
});
|
|
3348
|
+
const analysisPath = ws.directory + "/checks/analysis-" + inputs.fingerprint + ".json";
|
|
3349
|
+
await withLock(ws.root, ws.directory, async () => {
|
|
3350
|
+
const state = await loadState(ws);
|
|
3351
|
+
if (hash(state.baselines) !== hash(before.baselines) || state.analysis?.id !== before.analysis?.id) {
|
|
3352
|
+
throw new Error("Automation evidence changed during analysis; retry against the retained baselines.");
|
|
3353
|
+
}
|
|
3354
|
+
await writeStoreJson(ws.root, analysisPath, { analysis, digest: hash(analysis) });
|
|
3355
|
+
if (computed.cache) await writeStoreJson(ws.root, ws.directory + "/cache.json", computed.cache);
|
|
3356
|
+
state.baselines = computed.baselines;
|
|
3357
|
+
state.analysis = { id, fingerprint: inputs.fingerprint, completedAt: analysis.completedAt, path: analysisPath };
|
|
3358
|
+
await writeStoreJson(ws.root, ws.directory + "/state.json", state);
|
|
3359
|
+
});
|
|
3360
|
+
return { analysis, shared: false };
|
|
3361
|
+
}, Math.max(0, 25e3 - (Date.now() - startedAt)));
|
|
3362
|
+
}
|
|
2661
3363
|
async function automate(dir, event) {
|
|
3364
|
+
const startedAt = Date.now();
|
|
2662
3365
|
const ws = await workspace(dir);
|
|
2663
|
-
return
|
|
3366
|
+
return recordExecution(ws.root, ws.directory, event.event, async () => {
|
|
2664
3367
|
const inputs = await readInputs(ws.root);
|
|
2665
|
-
const
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
root: ws.root,
|
|
2671
|
-
gitDir: ws.gitDir,
|
|
2672
|
-
branch: ws.branch,
|
|
2673
|
-
baselines: [],
|
|
2674
|
-
sessions: {},
|
|
2675
|
-
updatedAt: now,
|
|
2676
|
-
fingerprint: null,
|
|
2677
|
-
latest: null
|
|
2678
|
-
} : parseState(raw);
|
|
2679
|
-
if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) {
|
|
2680
|
-
throw new Error("Automation state belongs to another branch or worktree; original evidence was retained.");
|
|
2681
|
-
}
|
|
2682
|
-
if (ws.branch === "detached" && state.latest) {
|
|
2683
|
-
const previous = await readStoreJson(ws.root, state.latest);
|
|
2684
|
-
if (!previous?.head || !/^[a-f0-9]{40,64}$/.test(previous.head)) throw new Error("The previous detached checkout evidence is unavailable.");
|
|
2685
|
-
try {
|
|
2686
|
-
await git2(ws.root, "merge-base", "--is-ancestor", previous.head, inputs.head);
|
|
2687
|
-
} catch {
|
|
2688
|
-
throw new Error("Detached checkout moved to a different history; original repair evidence was retained. Inspect that baseline explicitly.");
|
|
3368
|
+
const result = await analyze(ws, inputs, event, startedAt);
|
|
3369
|
+
if (result.shared) {
|
|
3370
|
+
await verifyInputs(ws, inputs);
|
|
3371
|
+
if (result.analysis.baselineDigest !== await baselineDigest(ws, await loadState(ws))) {
|
|
3372
|
+
throw new Error("Repair baseline was modified during shared automation; original evidence must be inspected.");
|
|
2689
3373
|
}
|
|
2690
3374
|
}
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
const
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
3375
|
+
return withLock(ws.root, ws.directory, async () => {
|
|
3376
|
+
const state = await loadState(ws);
|
|
3377
|
+
if (state.analysis?.id !== result.analysis.id) throw new Error("Automation evidence changed during publication; retry against the current inputs.");
|
|
3378
|
+
const report = structuredClone(result.analysis.report);
|
|
3379
|
+
if (result.shared) report.checks = { ...report.checks, ran: [], reused: [.../* @__PURE__ */ new Set([...report.checks.ran, ...report.checks.reused])], shared: true };
|
|
3380
|
+
report.reportPath = ws.directory + "/checks/" + randomUUID4() + ".json";
|
|
3381
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3382
|
+
const key = event.host && event.sessionId ? hash([event.host, event.sessionId]) : null;
|
|
3383
|
+
const newSession = key !== null && !state.sessions[key];
|
|
3384
|
+
if (key && !state.sessions[key]) {
|
|
3385
|
+
const keys = Object.keys(state.sessions).sort((a, b) => state.sessions[a].lastUsed.localeCompare(state.sessions[b].lastUsed));
|
|
3386
|
+
const removable = keys.filter((k) => !Object.keys(state.sessions[k].pending).length);
|
|
3387
|
+
for (const expired of removable.slice(0, Math.max(0, keys.length - 31))) delete state.sessions[expired];
|
|
3388
|
+
if (Object.keys(state.sessions).length >= 128) throw new Error("Too many sessions with unfinished tool calls; original evidence was retained.");
|
|
3389
|
+
state.sessions[key] = {
|
|
3390
|
+
host: event.host,
|
|
3391
|
+
seen: null,
|
|
3392
|
+
continued: false,
|
|
3393
|
+
initialIssues: report.findings.filter((f) => f.status === "unresolved").map((f) => f.id),
|
|
3394
|
+
initialDocs: inputs.docs,
|
|
3395
|
+
lastUsed: now,
|
|
3396
|
+
mutationObserved: false,
|
|
3397
|
+
pending: {},
|
|
3398
|
+
coverageGaps: [],
|
|
3399
|
+
events: {}
|
|
3400
|
+
};
|
|
2716
3401
|
}
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
3402
|
+
const session = key ? state.sessions[key] : null;
|
|
3403
|
+
if (session) {
|
|
3404
|
+
session.lastUsed = now;
|
|
3405
|
+
session.events[event.event] = { at: now, count: (session.events[event.event]?.count ?? 0) + 1 };
|
|
3406
|
+
if (event.event === "before_tool" && event.mutating && event.toolId) {
|
|
3407
|
+
if (Object.keys(session.pending).length >= 128) throw new Error("Too many unfinished tool calls to track pre-edit evidence.");
|
|
3408
|
+
session.pending[event.toolId] = inputs.fingerprint;
|
|
3409
|
+
}
|
|
3410
|
+
if (event.event === "after_tool" && event.mutating) {
|
|
3411
|
+
session.mutationObserved = true;
|
|
3412
|
+
if (!event.toolId || !session.pending[event.toolId]) {
|
|
3413
|
+
const gap = "A tool completed without an observed matching pre-tool capture; pre-edit coverage is unknown.";
|
|
3414
|
+
if (!session.coverageGaps.includes(gap)) session.coverageGaps.push(gap);
|
|
3415
|
+
}
|
|
3416
|
+
if (event.toolId) delete session.pending[event.toolId];
|
|
2722
3417
|
}
|
|
2723
|
-
|
|
3418
|
+
report.diagnostics.push(...session.coverageGaps);
|
|
3419
|
+
if (session.coverageGaps.length && report.status !== "unavailable") report.status = "incomplete";
|
|
3420
|
+
report.capture = report.status !== "unavailable" && !session.coverageGaps.length && (session.events.session_start || session.events.before_tool) ? "observed" : "unknown";
|
|
2724
3421
|
}
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
const
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
head: inputs.head,
|
|
2733
|
-
baselinePaths: [],
|
|
2734
|
-
reportPath: ws.directory + "/checks/" + randomUUID4() + ".json",
|
|
2735
|
-
findings: [],
|
|
2736
|
-
diagnostics: ["No AGENTS.md, CLAUDE.md, or .claude/CLAUDE.md exists. Documentation capture is unavailable; other Mason tools remain usable."],
|
|
2737
|
-
checks: { ran: [], reused: [], skipped: [] },
|
|
2738
|
-
counts: { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 },
|
|
2739
|
-
capture: "unknown",
|
|
2740
|
-
scope: SCOPE
|
|
2741
|
-
};
|
|
2742
|
-
const notify2 = !session || session.seen !== "no-docs";
|
|
2743
|
-
if (session) session.seen = "no-docs";
|
|
2744
|
-
state.fingerprint = inputs.fingerprint;
|
|
2745
|
-
state.latest = report2.reportPath;
|
|
2746
|
-
state.updatedAt = now;
|
|
2747
|
-
await writeStoreJson(ws.root, report2.reportPath, report2);
|
|
2748
|
-
await writeStoreJson(ws.root, statePath, state);
|
|
2749
|
-
return { report: report2, message: notify2 ? summarize(report2) : null, continueOnce: false };
|
|
2750
|
-
}
|
|
2751
|
-
let cached = null;
|
|
2752
|
-
const diagnostics = [];
|
|
2753
|
-
try {
|
|
2754
|
-
cached = await readStoreJson(ws.root, ws.directory + "/cache.json");
|
|
2755
|
-
} catch {
|
|
2756
|
-
diagnostics.push("Unreadable automation cache; checks are being recomputed.");
|
|
2757
|
-
}
|
|
2758
|
-
const cache = checkCache(cached, inputs);
|
|
2759
|
-
if (cache.diagnostic) diagnostics.push(cache.diagnostic);
|
|
2760
|
-
const saveBaseline = async () => {
|
|
2761
|
-
if (state.baselines.length >= 128) throw new Error("128 retained baselines need review; automatic capture stopped without discarding original evidence.");
|
|
2762
|
-
const prepared = await prepareRepair(ws.root, ALL_CHECKS, cache.options);
|
|
2763
|
-
state.baselines.push({ path: prepared.baselinePath, at: now, event: event.event, fingerprint: inputs.fingerprint });
|
|
2764
|
-
};
|
|
2765
|
-
if (!state.baselines.length) await saveBaseline();
|
|
2766
|
-
const verifications = [];
|
|
2767
|
-
for (const baseline of state.baselines) verifications.push(await verifyRepair(ws.root, baseline.path, cache.options));
|
|
2768
|
-
const known = new Set(verifications.flatMap((v) => v.findings.map((f) => f.id)));
|
|
2769
|
-
if (verifications.some((v) => v.newFindings.some((f) => !known.has(findingId(f))))) {
|
|
2770
|
-
await saveBaseline();
|
|
2771
|
-
verifications.push(await verifyRepair(ws.root, state.baselines.at(-1).path, cache.options));
|
|
2772
|
-
}
|
|
2773
|
-
const merged = /* @__PURE__ */ new Map();
|
|
2774
|
-
for (const verification of verifications) {
|
|
2775
|
-
for (const finding of verification.findings) {
|
|
2776
|
-
const previous = merged.get(finding.id);
|
|
2777
|
-
if (!previous || priority[finding.status] > priority[previous.status]) merged.set(finding.id, finding);
|
|
3422
|
+
const signature = hash([report.status, report.findings, report.diagnostics, report.checks.skipped]);
|
|
3423
|
+
const relevant = report.findings.some((f) => f.status === "unresolved" && session && (!session.initialIssues.includes(f.id) || session.initialDocs[f.original.anchor.doc] !== inputs.docs[f.original.anchor.doc]));
|
|
3424
|
+
const continueOnce = event.event === "task_end" && !!session?.mutationObserved && relevant && !session.continued && !event.stopHookActive;
|
|
3425
|
+
const notify = !session || newSession || signature !== session.seen || continueOnce;
|
|
3426
|
+
if (session) {
|
|
3427
|
+
session.seen = signature;
|
|
3428
|
+
if (continueOnce) session.continued = true;
|
|
2778
3429
|
}
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
if (after.fingerprint !== inputs.fingerprint || currentWs.directory !== ws.directory) {
|
|
2790
|
-
throw new Error("Repository inputs or branch changed during automation; no current verification was recorded. Retry on a stable checkout.");
|
|
2791
|
-
}
|
|
2792
|
-
const report = {
|
|
2793
|
-
version: 1,
|
|
2794
|
-
status: diagnostics.length || verifications.some((v) => v.status === "incomplete") ? "incomplete" : counts.unresolved ? "issues-remain" : "verified",
|
|
2795
|
-
root: ws.root,
|
|
2796
|
-
branch: ws.branch,
|
|
2797
|
-
head: inputs.head,
|
|
2798
|
-
baselinePaths: state.baselines.map((b) => b.path),
|
|
2799
|
-
reportPath: ws.directory + "/checks/" + randomUUID4() + ".json",
|
|
2800
|
-
findings: [...merged.values()],
|
|
2801
|
-
diagnostics: [...new Set(diagnostics)],
|
|
2802
|
-
checks: { ran: [...cache.ran], reused: [...cache.reused].filter((name) => !cache.ran.has(name)), skipped: current?.skippedChecks ?? [] },
|
|
2803
|
-
counts,
|
|
2804
|
-
capture,
|
|
2805
|
-
scope: SCOPE
|
|
2806
|
-
};
|
|
2807
|
-
const signature = hash([report.status, report.findings, report.diagnostics, report.checks.skipped]);
|
|
2808
|
-
const relevant = report.findings.some((f) => f.status === "unresolved" && session && (!session.initialIssues.includes(f.id) || session.initialDocs[f.original.anchor.doc] !== inputs.docs[f.original.anchor.doc]));
|
|
2809
|
-
const continueOnce = event.event === "task_end" && !!session?.mutationObserved && relevant && !session.continued && !event.stopHookActive;
|
|
2810
|
-
const notify = !session || newSession || signature !== session.seen || continueOnce;
|
|
2811
|
-
if (session) {
|
|
2812
|
-
session.seen = signature;
|
|
2813
|
-
if (continueOnce) session.continued = true;
|
|
2814
|
-
}
|
|
2815
|
-
const persistReport = !state.latest || state.fingerprint !== inputs.fingerprint || notify || event.event === "task_end";
|
|
2816
|
-
if (!persistReport) report.reportPath = state.latest;
|
|
2817
|
-
state.updatedAt = now;
|
|
2818
|
-
state.fingerprint = inputs.fingerprint;
|
|
2819
|
-
state.latest = report.reportPath;
|
|
2820
|
-
if (persistReport) await writeStoreJson(ws.root, report.reportPath, report);
|
|
2821
|
-
if (cache.ran.size || cached === null || cache.diagnostic) await writeStoreJson(ws.root, ws.directory + "/cache.json", cache.serialize());
|
|
2822
|
-
await writeStoreJson(ws.root, statePath, state);
|
|
2823
|
-
return { report, message: notify ? summarize(report) : null, continueOnce };
|
|
2824
|
-
}));
|
|
3430
|
+
const persistReport = !state.latest || state.fingerprint !== inputs.fingerprint || notify || event.event === "task_end";
|
|
3431
|
+
if (!persistReport) report.reportPath = state.latest;
|
|
3432
|
+
state.updatedAt = now;
|
|
3433
|
+
state.fingerprint = inputs.fingerprint;
|
|
3434
|
+
state.latest = report.reportPath;
|
|
3435
|
+
if (persistReport) await writeStoreJson(ws.root, report.reportPath, report);
|
|
3436
|
+
await writeStoreJson(ws.root, ws.directory + "/state.json", state);
|
|
3437
|
+
return { report, message: notify ? summarize(report) : null, continueOnce };
|
|
3438
|
+
});
|
|
3439
|
+
});
|
|
2825
3440
|
}
|
|
2826
3441
|
async function automationStatus(dir) {
|
|
2827
3442
|
const ws = await workspace(dir);
|
|
2828
3443
|
const execution = await executionStatus(ws.root, ws.directory);
|
|
3444
|
+
const notifications = await failureNotifications(ws.root, ws.directory);
|
|
2829
3445
|
const raw = await readStoreJson(ws.root, ws.directory + "/state.json");
|
|
2830
|
-
if (raw === null) return { version: 1, status: execution.status === "not-observed" ? "not-observed" : "unavailable", root: ws.root, branch: ws.branch, baselinePaths: [], hosts: {}, execution };
|
|
3446
|
+
if (raw === null) return { version: 1, status: execution.status === "not-observed" ? "not-observed" : "unavailable", root: ws.root, branch: ws.branch, baselinePaths: [], hosts: {}, execution, notifications };
|
|
2831
3447
|
const state = parseState(raw);
|
|
2832
3448
|
if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) throw new Error("Automation state belongs to another workspace.");
|
|
2833
3449
|
const inputs = await readInputs(ws.root);
|
|
@@ -2849,10 +3465,11 @@ async function automationStatus(dir) {
|
|
|
2849
3465
|
verificationStatus: unfinished ? "unavailable" : latest?.status ?? "unavailable",
|
|
2850
3466
|
hosts: hosts2,
|
|
2851
3467
|
execution,
|
|
3468
|
+
notifications,
|
|
2852
3469
|
note: "Observed events do not prove all tool paths are intercepted. Run check to verify the retained evidence."
|
|
2853
3470
|
};
|
|
2854
3471
|
}
|
|
2855
|
-
var SCOPE, priority, cleanText;
|
|
3472
|
+
var SCOPE, priority, cleanText, analysisSchema, sharedSchema, baselineDigest;
|
|
2856
3473
|
var init_runtime = __esm({
|
|
2857
3474
|
"src/automation/runtime.ts"() {
|
|
2858
3475
|
"use strict";
|
|
@@ -2862,14 +3479,43 @@ var init_runtime = __esm({
|
|
|
2862
3479
|
init_storage();
|
|
2863
3480
|
init_evidence();
|
|
2864
3481
|
init_store();
|
|
2865
|
-
SCOPE = "Documentation audit evidence only. Hook receipts show observed events, not complete interception. Resolved
|
|
3482
|
+
SCOPE = "Documentation audit evidence only. Hook receipts show observed events, not complete interception. Resolved findings no longer fail their checks or have a recorded assessment covering current evidence. Repair and review only within the user's task authorization.";
|
|
2866
3483
|
priority = { resolved: 0, "review-required": 1, unresolved: 2, unverified: 3 };
|
|
2867
3484
|
cleanText = (text2) => text2.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 250);
|
|
3485
|
+
analysisSchema = z10.object({
|
|
3486
|
+
id: z10.string(),
|
|
3487
|
+
fingerprint: z10.string(),
|
|
3488
|
+
completedAt: z10.number(),
|
|
3489
|
+
baselineDigest: z10.string(),
|
|
3490
|
+
report: z10.object({
|
|
3491
|
+
version: z10.literal(1),
|
|
3492
|
+
status: z10.enum(["verified", "issues-remain", "incomplete", "unavailable"]),
|
|
3493
|
+
root: z10.string(),
|
|
3494
|
+
branch: z10.string(),
|
|
3495
|
+
head: z10.string(),
|
|
3496
|
+
baselinePaths: z10.array(z10.string()),
|
|
3497
|
+
reportPath: z10.string(),
|
|
3498
|
+
findings: z10.array(repairFindingSchema),
|
|
3499
|
+
diagnostics: z10.array(z10.string()),
|
|
3500
|
+
checks: z10.object({
|
|
3501
|
+
ran: z10.array(z10.string()),
|
|
3502
|
+
reused: z10.array(z10.string()),
|
|
3503
|
+
skipped: z10.array(z10.object({ check: z10.string(), reason: z10.string(), doc: z10.string().optional() }))
|
|
3504
|
+
}),
|
|
3505
|
+
counts: z10.object({ resolved: z10.number(), unresolved: z10.number(), "review-required": z10.number(), unverified: z10.number() }),
|
|
3506
|
+
capture: z10.enum(["observed", "unknown"]),
|
|
3507
|
+
scope: z10.string()
|
|
3508
|
+
})
|
|
3509
|
+
});
|
|
3510
|
+
sharedSchema = z10.object({ analysis: analysisSchema, digest: z10.string() });
|
|
3511
|
+
baselineDigest = async (ws, state) => hash(await Promise.all(
|
|
3512
|
+
state.baselines.map(async (b) => [b.path, await readStoreJson(ws.root, b.path)])
|
|
3513
|
+
));
|
|
2868
3514
|
}
|
|
2869
3515
|
});
|
|
2870
3516
|
|
|
2871
3517
|
// src/setup/model.ts
|
|
2872
|
-
import { z as
|
|
3518
|
+
import { z as z11 } from "zod";
|
|
2873
3519
|
async function loadSetup(root) {
|
|
2874
3520
|
const raw = await readStoreJson(root, SETUP_PATH);
|
|
2875
3521
|
if (raw === null) return null;
|
|
@@ -2887,42 +3533,42 @@ var init_model = __esm({
|
|
|
2887
3533
|
"src/setup/model.ts"() {
|
|
2888
3534
|
"use strict";
|
|
2889
3535
|
init_storage();
|
|
2890
|
-
setupHostSchema =
|
|
2891
|
-
configuredVersion:
|
|
2892
|
-
revision:
|
|
2893
|
-
fingerprint:
|
|
2894
|
-
mcpFingerprint:
|
|
2895
|
-
instructions:
|
|
3536
|
+
setupHostSchema = z11.object({
|
|
3537
|
+
configuredVersion: z11.string(),
|
|
3538
|
+
revision: z11.string().uuid(),
|
|
3539
|
+
fingerprint: z11.string(),
|
|
3540
|
+
mcpFingerprint: z11.string(),
|
|
3541
|
+
instructions: z11.array(z11.string())
|
|
2896
3542
|
});
|
|
2897
|
-
setupSchema =
|
|
3543
|
+
setupSchema = z11.object({ version: z11.literal(2), hosts: z11.object({
|
|
2898
3544
|
codex: setupHostSchema.optional(),
|
|
2899
3545
|
claude: setupHostSchema.optional()
|
|
2900
3546
|
}) });
|
|
2901
3547
|
SETUP_PATH = ".mason/local/setup.json";
|
|
2902
|
-
receiptSchema =
|
|
2903
|
-
version:
|
|
2904
|
-
host:
|
|
2905
|
-
status:
|
|
2906
|
-
initialReportPath:
|
|
2907
|
-
initialBaselinePaths:
|
|
2908
|
-
root:
|
|
2909
|
-
revision:
|
|
2910
|
-
configuredAt:
|
|
3548
|
+
receiptSchema = z11.object({
|
|
3549
|
+
version: z11.literal(1),
|
|
3550
|
+
host: z11.enum(["codex", "claude"]),
|
|
3551
|
+
status: z11.enum(["installing", "configured"]),
|
|
3552
|
+
initialReportPath: z11.string(),
|
|
3553
|
+
initialBaselinePaths: z11.array(z11.string()),
|
|
3554
|
+
root: z11.string(),
|
|
3555
|
+
revision: z11.string().uuid(),
|
|
3556
|
+
configuredAt: z11.string().optional()
|
|
2911
3557
|
});
|
|
2912
3558
|
}
|
|
2913
3559
|
});
|
|
2914
3560
|
|
|
2915
3561
|
// src/setup/launcher.ts
|
|
2916
|
-
import { execFile as
|
|
2917
|
-
import { promisify as
|
|
2918
|
-
import
|
|
3562
|
+
import { execFile as execFile10 } from "child_process";
|
|
3563
|
+
import { promisify as promisify10 } from "util";
|
|
3564
|
+
import fs15 from "fs/promises";
|
|
2919
3565
|
function globalCommand(args, platform = process.platform) {
|
|
2920
3566
|
return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args] } : { command: "mason", args };
|
|
2921
3567
|
}
|
|
2922
3568
|
async function installedCommand() {
|
|
2923
3569
|
const invocation = globalCommand(["internal-integration-version"]);
|
|
2924
3570
|
try {
|
|
2925
|
-
const { stdout } = await
|
|
3571
|
+
const { stdout } = await promisify10(execFile10)(
|
|
2926
3572
|
invocation.command,
|
|
2927
3573
|
invocation.args,
|
|
2928
3574
|
{ timeout: 5e3, maxBuffer: 8192, windowsHide: true }
|
|
@@ -2940,7 +3586,7 @@ var init_launcher = __esm({
|
|
|
2940
3586
|
"use strict";
|
|
2941
3587
|
init_evidence();
|
|
2942
3588
|
init_model();
|
|
2943
|
-
executingVersion = () => true ? "0.
|
|
3589
|
+
executingVersion = () => true ? "0.17.0" : "development";
|
|
2944
3590
|
mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
|
|
2945
3591
|
hookCommand = (host) => `mason --setup-host ${host} auto`;
|
|
2946
3592
|
}
|
|
@@ -2953,8 +3599,8 @@ __export(observations_exports, {
|
|
|
2953
3599
|
observeActivation: () => observeActivation,
|
|
2954
3600
|
readObservation: () => readObservation
|
|
2955
3601
|
});
|
|
2956
|
-
import
|
|
2957
|
-
import { z as
|
|
3602
|
+
import fs16 from "fs/promises";
|
|
3603
|
+
import { z as z12 } from "zod";
|
|
2958
3604
|
function observationPath(directory, host, revision, version = executingVersion()) {
|
|
2959
3605
|
return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
|
|
2960
3606
|
}
|
|
@@ -2970,8 +3616,8 @@ async function observeActivation(dir, event, options = {}) {
|
|
|
2970
3616
|
if (host !== "codex" && host !== "claude" || !revision || !process.env.MASON_SETUP_ROOT) return null;
|
|
2971
3617
|
try {
|
|
2972
3618
|
const capturedDirectory = options.directory?.match(/^\.mason\/reports\/automation\/[a-f0-9]{24}$/)?.[0] ?? options.reportPath?.match(/^(\.mason\/reports\/automation\/[a-f0-9]{24})\/checks\//)?.[1];
|
|
2973
|
-
const ws = capturedDirectory ? { root: await
|
|
2974
|
-
if (ws.root !== await
|
|
3619
|
+
const ws = capturedDirectory ? { root: await fs16.realpath(dir), directory: capturedDirectory } : await workspace(dir);
|
|
3620
|
+
if (ws.root !== await fs16.realpath(process.env.MASON_SETUP_ROOT)) return null;
|
|
2975
3621
|
const setup = await loadSetup(ws.root);
|
|
2976
3622
|
if (setup?.hosts[host]?.revision !== revision) return "Mason setup changed; restart the assistant to observe the current integration.";
|
|
2977
3623
|
const directory = ws.directory + "/activation";
|
|
@@ -3019,26 +3665,26 @@ var init_observations = __esm({
|
|
|
3019
3665
|
init_storage();
|
|
3020
3666
|
init_model();
|
|
3021
3667
|
init_launcher();
|
|
3022
|
-
observationSchema =
|
|
3023
|
-
version:
|
|
3024
|
-
masonVersion:
|
|
3025
|
-
root:
|
|
3026
|
-
revision:
|
|
3027
|
-
host:
|
|
3028
|
-
contextCalls:
|
|
3029
|
-
lastContextAt:
|
|
3030
|
-
sessions:
|
|
3031
|
-
events:
|
|
3032
|
-
at:
|
|
3033
|
-
verificationStatus:
|
|
3034
|
-
reportPath:
|
|
3668
|
+
observationSchema = z12.object({
|
|
3669
|
+
version: z12.literal(1),
|
|
3670
|
+
masonVersion: z12.string(),
|
|
3671
|
+
root: z12.string(),
|
|
3672
|
+
revision: z12.string(),
|
|
3673
|
+
host: z12.enum(["codex", "claude"]),
|
|
3674
|
+
contextCalls: z12.number().int().nonnegative(),
|
|
3675
|
+
lastContextAt: z12.string().optional(),
|
|
3676
|
+
sessions: z12.record(z12.object({
|
|
3677
|
+
events: z12.array(z12.enum(events)),
|
|
3678
|
+
at: z12.string(),
|
|
3679
|
+
verificationStatus: z12.string().optional(),
|
|
3680
|
+
reportPath: z12.string().optional()
|
|
3035
3681
|
}))
|
|
3036
3682
|
});
|
|
3037
3683
|
}
|
|
3038
3684
|
});
|
|
3039
3685
|
|
|
3040
3686
|
// src/automation/adapters.ts
|
|
3041
|
-
import { z as
|
|
3687
|
+
import { z as z13 } from "zod";
|
|
3042
3688
|
function normalizeHook(host, raw) {
|
|
3043
3689
|
hostSchema.parse(host);
|
|
3044
3690
|
const input2 = inputSchema.parse(raw);
|
|
@@ -3054,6 +3700,7 @@ function normalizeHook(host, raw) {
|
|
|
3054
3700
|
}
|
|
3055
3701
|
async function runAutomationHook(host, stdin) {
|
|
3056
3702
|
let name = "";
|
|
3703
|
+
let hook;
|
|
3057
3704
|
try {
|
|
3058
3705
|
if (Buffer.byteLength(stdin) > 1024 * 1024) throw new Error("Hook input exceeds 1 MiB.");
|
|
3059
3706
|
let raw;
|
|
@@ -3063,6 +3710,7 @@ async function runAutomationHook(host, stdin) {
|
|
|
3063
3710
|
throw new Error("Hook input is not valid JSON.");
|
|
3064
3711
|
}
|
|
3065
3712
|
const input2 = normalizeHook(host, raw);
|
|
3713
|
+
hook = input2;
|
|
3066
3714
|
name = input2.name;
|
|
3067
3715
|
if (["before_tool", "after_tool"].includes(input2.event.event) && input2.readOnly) {
|
|
3068
3716
|
const observed = await observeReadOnlyTool(input2.cwd, input2.event);
|
|
@@ -3084,7 +3732,8 @@ async function runAutomationHook(host, stdin) {
|
|
|
3084
3732
|
}
|
|
3085
3733
|
return { hookSpecificOutput: { hookEventName: name, additionalContext: result.message } };
|
|
3086
3734
|
} catch (error) {
|
|
3087
|
-
const message2 = failureMessage(error);
|
|
3735
|
+
const message2 = hook ? await hookFailureMessage(hook.cwd, host, hook.event.sessionId, error) : failureMessage(error);
|
|
3736
|
+
if (!message2) return null;
|
|
3088
3737
|
return { systemMessage: message2, ...["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse"].includes(name) ? { hookSpecificOutput: { hookEventName: name, additionalContext: message2 } } : {} };
|
|
3089
3738
|
}
|
|
3090
3739
|
}
|
|
@@ -3109,15 +3758,15 @@ var init_adapters = __esm({
|
|
|
3109
3758
|
init_execution();
|
|
3110
3759
|
init_runtime();
|
|
3111
3760
|
init_store();
|
|
3112
|
-
inputSchema =
|
|
3113
|
-
cwd:
|
|
3114
|
-
session_id:
|
|
3115
|
-
hook_event_name:
|
|
3116
|
-
tool_name:
|
|
3117
|
-
tool_use_id:
|
|
3118
|
-
tool_input:
|
|
3119
|
-
stop_hook_active:
|
|
3120
|
-
permission_mode:
|
|
3761
|
+
inputSchema = z13.object({
|
|
3762
|
+
cwd: z13.string().min(1),
|
|
3763
|
+
session_id: z13.string().min(1).max(500),
|
|
3764
|
+
hook_event_name: z13.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"]),
|
|
3765
|
+
tool_name: z13.string().optional(),
|
|
3766
|
+
tool_use_id: z13.string().max(500).optional(),
|
|
3767
|
+
tool_input: z13.unknown().optional(),
|
|
3768
|
+
stop_hook_active: z13.boolean().optional(),
|
|
3769
|
+
permission_mode: z13.string().optional()
|
|
3121
3770
|
});
|
|
3122
3771
|
lifecycle = {
|
|
3123
3772
|
SessionStart: "session_start",
|
|
@@ -3137,8 +3786,8 @@ __export(files_exports, {
|
|
|
3137
3786
|
managedBlock: () => managedBlock,
|
|
3138
3787
|
readText: () => readText
|
|
3139
3788
|
});
|
|
3140
|
-
import
|
|
3141
|
-
import
|
|
3789
|
+
import fs17 from "fs/promises";
|
|
3790
|
+
import path21 from "path";
|
|
3142
3791
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
3143
3792
|
async function readText(root, file) {
|
|
3144
3793
|
try {
|
|
@@ -3167,14 +3816,14 @@ async function applyEdit(root, edit) {
|
|
|
3167
3816
|
if (edit.after === null) {
|
|
3168
3817
|
const file2 = await storePath(root, edit.path);
|
|
3169
3818
|
if (await readText(root, edit.path) !== edit.before) throw new Error("Input changed during removal: " + edit.path);
|
|
3170
|
-
await
|
|
3819
|
+
await fs17.unlink(file2);
|
|
3171
3820
|
return true;
|
|
3172
3821
|
}
|
|
3173
3822
|
const file = await storePath(root, edit.path, true);
|
|
3174
|
-
const temporary =
|
|
3823
|
+
const temporary = path21.join(path21.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
|
|
3175
3824
|
try {
|
|
3176
|
-
const mode = await
|
|
3177
|
-
const handle = await
|
|
3825
|
+
const mode = await fs17.stat(file).then((s) => s.mode & 511, () => 384);
|
|
3826
|
+
const handle = await fs17.open(temporary, "wx", mode);
|
|
3178
3827
|
try {
|
|
3179
3828
|
await handle.writeFile(edit.after, "utf8");
|
|
3180
3829
|
await handle.sync();
|
|
@@ -3182,10 +3831,10 @@ async function applyEdit(root, edit) {
|
|
|
3182
3831
|
await handle.close();
|
|
3183
3832
|
}
|
|
3184
3833
|
if (await readText(root, edit.path) !== edit.before) throw new Error("Integration input changed during update: " + edit.path);
|
|
3185
|
-
await
|
|
3834
|
+
await fs17.rename(temporary, file);
|
|
3186
3835
|
return true;
|
|
3187
3836
|
} finally {
|
|
3188
|
-
await
|
|
3837
|
+
await fs17.rm(temporary, { force: true });
|
|
3189
3838
|
}
|
|
3190
3839
|
}
|
|
3191
3840
|
var init_files2 = __esm({
|
|
@@ -3206,7 +3855,7 @@ __export(ownership_exports, {
|
|
|
3206
3855
|
ownershipEdit: () => ownershipEdit,
|
|
3207
3856
|
ownershipSchema: () => ownershipSchema
|
|
3208
3857
|
});
|
|
3209
|
-
import { z as
|
|
3858
|
+
import { z as z14 } from "zod";
|
|
3210
3859
|
import { parse } from "smol-toml";
|
|
3211
3860
|
function blockSpan(text2, start, end) {
|
|
3212
3861
|
const starts = text2.split(start).length - 1, ends = text2.split(end).length - 1;
|
|
@@ -3257,30 +3906,30 @@ var init_ownership = __esm({
|
|
|
3257
3906
|
["<!-- mason:agents:start -->", "<!-- mason:agents:end -->"],
|
|
3258
3907
|
["# mason:mcp:start", "# mason:mcp:end"]
|
|
3259
3908
|
];
|
|
3260
|
-
fileSchema =
|
|
3261
|
-
created:
|
|
3262
|
-
initialHash:
|
|
3263
|
-
blocks:
|
|
3264
|
-
mcpHash:
|
|
3265
|
-
hookCommands:
|
|
3266
|
-
hooks:
|
|
3909
|
+
fileSchema = z14.object({
|
|
3910
|
+
created: z14.boolean(),
|
|
3911
|
+
initialHash: z14.string(),
|
|
3912
|
+
blocks: z14.record(z14.string()),
|
|
3913
|
+
mcpHash: z14.string().optional(),
|
|
3914
|
+
hookCommands: z14.array(z14.string()).optional(),
|
|
3915
|
+
hooks: z14.record(z14.array(z14.string())).optional()
|
|
3267
3916
|
});
|
|
3268
|
-
ownershipSchema =
|
|
3917
|
+
ownershipSchema = z14.object({ version: z14.literal(1), files: z14.record(fileSchema) });
|
|
3269
3918
|
}
|
|
3270
3919
|
});
|
|
3271
3920
|
|
|
3272
3921
|
// src/mcp/init.ts
|
|
3273
|
-
import { z as
|
|
3922
|
+
import { z as z15 } from "zod";
|
|
3274
3923
|
var markerSchema, CLAUDE_MD_SECTION, ASSISTANT_SETUP, QUICKSTART_PLAYBOOK, MAP_PLAYBOOK;
|
|
3275
3924
|
var init_init = __esm({
|
|
3276
3925
|
"src/mcp/init.ts"() {
|
|
3277
3926
|
"use strict";
|
|
3278
3927
|
init_storage();
|
|
3279
3928
|
init_files2();
|
|
3280
|
-
markerSchema =
|
|
3281
|
-
version:
|
|
3282
|
-
initializedAt:
|
|
3283
|
-
features:
|
|
3929
|
+
markerSchema = z15.object({
|
|
3930
|
+
version: z15.literal(1),
|
|
3931
|
+
initializedAt: z15.string(),
|
|
3932
|
+
features: z15.object({ confluence: z15.boolean().optional() }).passthrough().optional()
|
|
3284
3933
|
}).passthrough();
|
|
3285
3934
|
CLAUDE_MD_SECTION = `<!-- mason:start -->
|
|
3286
3935
|
## Mason project knowledge
|
|
@@ -3290,12 +3939,14 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
|
|
|
3290
3939
|
- Task, bug, change request, or investigation of why the project works this way \u2192 \`get_context\` with the task text and known files: matching decisions, related tests, impact, and any available map entries.
|
|
3291
3940
|
- Before editing a file \u2192 \`get_impact\` to check references, tests, and historical change partners.
|
|
3292
3941
|
- Investigation or correction reveals a reusable project lesson \u2192 \`save_decision\` before finishing. Capture only the lasting constraint and reason, with anchors and known sources, owner, and recorder. This creates a proposal; never infer approval. Preserve uncertain causes and unknown attribution. Route project lessons here; keep personal preferences in agent memory. Skip code summaries, current checkout status, temporary task permissions, session trivia, and secrets. No qualifying lesson means no record.
|
|
3942
|
+
- Before saving, compare the lesson with matching \`get_context\` records and any pending proposal. If new evidence changes the same lesson's assumptions, scope, or recommended action, revise it with \`save_decision(id: existingId)\`, including when it is accepted. Preserve supported rationale and sources, replace obsolete instructions, and add the new evidence. Create a separate record for a genuinely distinct lesson; skip unchanged restatements. Revision preserves history and leaves the accepted version operative until authorized review.
|
|
3293
3943
|
- Consult trust metadata before relying on entries: unknown or changed freshness requires inspection, and failed verification means the description must be corrected. Check approval too: proposals are suggestions, legacy records are unreviewed, and accepted decisions are recorded constraints subject to freshness checks. An accepted revision remains operative while a pending proposal is reviewed; keep both versions and their freshness distinct.
|
|
3294
3944
|
- Asked to review or re-verify a decision \u2192 \`review_decision\` first to inspect content, sources, history, and code changes. Record acceptance, reaffirmation, or retirement only when authorized by the user or a cited team review, with the actual reviewer and reason. Never infer approval from unchanged code. Review and commit the local record through the normal project workflow.
|
|
3295
3945
|
- For an architectural overview, use \`get_snapshot\` if a map is available. If \`map.status\` is missing or invalid, use available decisions and source evidence; do not start building a map unless requested.
|
|
3296
3946
|
- \`mason_init\` returns documentation audit and committed-diff review results, plus a short setup guide. Pass \`evidence\` with local CI manifest paths to include test and analysis results; the CLI equivalent is \`mason-review --evidence <manifest>\`. State skipped, unavailable, stale, or unknown checks explicitly. Related accepted decisions identify review context, not proven violations.
|
|
3297
3947
|
|
|
3298
3948
|
- When documentation repair is authorized, use \`mason_repair(action: "prepare")\` before edits, keep its baselinePath, and use \`mason_repair(action: "verify", baselinePath)\` after edits and any final doc commit. Report every original finding's outcome and any new findings. Suppressed advisories remain unresolved; editing a doc does not approve it.
|
|
3949
|
+
- When advisory assessment is authorized, use \`review_advisory(action: "prepare", baselinePath, findingId)\` to inspect original evidence and history. Record \`addressed\`, \`inapplicable\`, or \`deferred\` with the actual reviewer, reason in note, and returned reviewToken only after relevant edits are committed. Commit the assessment and verify the same baseline again. Relevant evidence changes reopen it; deferral stays outstanding. Decision findings use \`review_decision\`; advisory closure never approves a decision.
|
|
3299
3950
|
|
|
3300
3951
|
- When Mason automation is installed, use \`mason_automation(action: "status")\` to inspect configured hooks and observed events, and \`mason_automation(action: "check")\` to resume its retained repair evidence. CLI fallback: \`mason-auto status\` / \`mason-auto check\`. Preserve existing baselines across sessions. Automatic checks do not authorize unrelated repairs or approve advisories.
|
|
3301
3952
|
|
|
@@ -3311,7 +3962,7 @@ ${CLAUDE_MD_SECTION}`;
|
|
|
3311
3962
|
|
|
3312
3963
|
1. Explain the actionable findings with their source evidence. Separate audit issues, advisories, and skipped checks. The review covers committed changes from the merge base to HEAD; workingTree paths are not included in that review. An unavailable or empty check is not proof that the project is correct. Use the CLI for full output if a summary is truncated.
|
|
3313
3964
|
2. Address findings within the user's requested scope. Setup alone authorizes Mason host configuration, hooks, instructions, and ignore rules; rewriting existing project claims requires repair scope. If repair is authorized, call \`mason_repair(dir, action: "prepare")\` before editing; it saves the full original findings even when this summary is truncated. Inspect relevant source, make grounded edits, then call \`mason_repair(dir, action: "verify", baselinePath)\` with that same baseline, including after any final doc commit. Report resolved, unresolved, review-required, unverified, and new findings. Keep suppressed advisories visible even when setup has already dirtied a doc. Do not invent a decision just to populate the store.
|
|
3314
|
-
3. When the task reveals a real lesson or constraint,
|
|
3965
|
+
3. When the task reveals a real lesson or constraint, compare matching \`get_context\` records first. If its assumptions, scope, or recommended action changed, revise that record using \`save_decision(id: existingId)\`; create a separate record for a genuinely distinct lesson and skip unchanged restatements. Include title, body, category, anchors, and known owner/source/actor information. Preserve supported rationale and sources, replace obsolete instructions, and add new evidence. Missing attribution can be added later. The tool writes a local proposal; editing it preserves revision history and requires a new acceptance, while any earlier accepted revision remains operative. An unchanged save never refreshes its evidence. When decision review is requested, \`review_decision\` prepares the record and code evidence before any authorized verdict. Review and commit records through the normal workflow. Retrieve it on the next relevant task with \`get_context(dir, task, files)\`.
|
|
3315
3966
|
|
|
3316
3967
|
${ASSISTANT_SETUP}
|
|
3317
3968
|
|
|
@@ -3357,8 +4008,8 @@ var git_exports = {};
|
|
|
3357
4008
|
__export(git_exports, {
|
|
3358
4009
|
isGitRepo: () => isGitRepo
|
|
3359
4010
|
});
|
|
3360
|
-
import { execFile as
|
|
3361
|
-
import { promisify as
|
|
4011
|
+
import { execFile as execFile11 } from "child_process";
|
|
4012
|
+
import { promisify as promisify11 } from "util";
|
|
3362
4013
|
async function isGitRepo(dir) {
|
|
3363
4014
|
try {
|
|
3364
4015
|
await exec8("git", ["rev-parse", "--git-dir"], { cwd: dir });
|
|
@@ -3371,7 +4022,7 @@ var exec8;
|
|
|
3371
4022
|
var init_git2 = __esm({
|
|
3372
4023
|
"src/utils/git.ts"() {
|
|
3373
4024
|
"use strict";
|
|
3374
|
-
exec8 =
|
|
4025
|
+
exec8 = promisify11(execFile11);
|
|
3375
4026
|
}
|
|
3376
4027
|
});
|
|
3377
4028
|
|
|
@@ -3387,7 +4038,7 @@ __export(config_exports, {
|
|
|
3387
4038
|
});
|
|
3388
4039
|
import { isDeepStrictEqual } from "util";
|
|
3389
4040
|
import { parse as parse2, stringify } from "smol-toml";
|
|
3390
|
-
import { z as
|
|
4041
|
+
import { z as z16 } from "zod";
|
|
3391
4042
|
async function instructionEdits(root, host) {
|
|
3392
4043
|
const found = await Promise.all(DOC_CANDIDATES.map(async (file) => ({ path: file, text: await readText(root, file) })));
|
|
3393
4044
|
const existing = found.find((f) => f.text !== null);
|
|
@@ -3456,11 +4107,11 @@ async function hookEdits(root, host) {
|
|
|
3456
4107
|
}
|
|
3457
4108
|
async function ancillaryEdits(root) {
|
|
3458
4109
|
const ignore = await readText(root, ".gitignore");
|
|
3459
|
-
const { git:
|
|
4110
|
+
const { git: git4 } = await Promise.resolve().then(() => (init_evidence(), evidence_exports));
|
|
3460
4111
|
const { isGitRepo: isGitRepo2 } = await Promise.resolve().then(() => (init_git2(), git_exports));
|
|
3461
4112
|
let parentIgnored = false;
|
|
3462
4113
|
try {
|
|
3463
|
-
if (await isGitRepo2(root)) parentIgnored = !!(await
|
|
4114
|
+
if (await isGitRepo2(root)) parentIgnored = !!(await git4(root, "check-ignore", "--no-index", ".mason")).trim();
|
|
3464
4115
|
} catch (error) {
|
|
3465
4116
|
if (error.code !== 1) throw error;
|
|
3466
4117
|
}
|
|
@@ -3469,6 +4120,8 @@ async function ancillaryEdits(root) {
|
|
|
3469
4120
|
...retainParentRule ? ["!/.mason/", "/.mason/*"] : [],
|
|
3470
4121
|
"!/.mason/decisions/",
|
|
3471
4122
|
"!/.mason/decisions/**",
|
|
4123
|
+
"!/.mason/reviews/",
|
|
4124
|
+
"!/.mason/reviews/**",
|
|
3472
4125
|
"!/.mason/config.json",
|
|
3473
4126
|
"!/.mason/snapshot.json",
|
|
3474
4127
|
"/.mason/local/",
|
|
@@ -3503,7 +4156,7 @@ var init_config = __esm({
|
|
|
3503
4156
|
init_install();
|
|
3504
4157
|
init_files2();
|
|
3505
4158
|
init_launcher();
|
|
3506
|
-
object =
|
|
4159
|
+
object = z16.record(z16.unknown());
|
|
3507
4160
|
mcpPath = (host) => host === "codex" ? ".codex/config.toml" : ".mcp.json";
|
|
3508
4161
|
TOML_START = "# mason:mcp:start";
|
|
3509
4162
|
TOML_END = "# mason:mcp:end";
|
|
@@ -3521,11 +4174,11 @@ __export(install_exports, {
|
|
|
3521
4174
|
installAutomation: () => installAutomation,
|
|
3522
4175
|
installedAutomation: () => installedAutomation,
|
|
3523
4176
|
planAutomationInstall: () => planAutomationInstall,
|
|
3524
|
-
recordSchema: () =>
|
|
4177
|
+
recordSchema: () => recordSchema2
|
|
3525
4178
|
});
|
|
3526
|
-
import { z as
|
|
4179
|
+
import { z as z17 } from "zod";
|
|
3527
4180
|
async function installationRecord(root) {
|
|
3528
|
-
return
|
|
4181
|
+
return recordSchema2.parse(await readStoreJson(root, AUTOMATION_PATH) ?? { version: 1, hosts: {} });
|
|
3529
4182
|
}
|
|
3530
4183
|
async function installAutomation(dir, host, command) {
|
|
3531
4184
|
const ws = await workspace(dir);
|
|
@@ -3594,7 +4247,7 @@ async function installedAutomation(dir) {
|
|
|
3594
4247
|
}
|
|
3595
4248
|
return result;
|
|
3596
4249
|
}
|
|
3597
|
-
var groupSchema, automationConfigSchema,
|
|
4250
|
+
var groupSchema, automationConfigSchema, recordSchema2, AUTOMATION_PATH, configPath;
|
|
3598
4251
|
var init_install = __esm({
|
|
3599
4252
|
"src/automation/install.ts"() {
|
|
3600
4253
|
"use strict";
|
|
@@ -3604,9 +4257,9 @@ var init_install = __esm({
|
|
|
3604
4257
|
init_store();
|
|
3605
4258
|
init_files2();
|
|
3606
4259
|
init_ownership();
|
|
3607
|
-
groupSchema =
|
|
3608
|
-
automationConfigSchema =
|
|
3609
|
-
|
|
4260
|
+
groupSchema = z17.object({ hooks: z17.array(z17.object({ type: z17.string(), command: z17.string().optional() }).passthrough()) }).passthrough();
|
|
4261
|
+
automationConfigSchema = z17.object({ hooks: z17.record(z17.array(groupSchema)).optional() }).passthrough();
|
|
4262
|
+
recordSchema2 = z17.object({ version: z17.literal(1), hosts: z17.record(z17.object({ command: z17.string() })) });
|
|
3610
4263
|
AUTOMATION_PATH = ".mason/local/automation.json";
|
|
3611
4264
|
configPath = (host) => host === "claude" ? ".claude/settings.json" : ".codex/hooks.json";
|
|
3612
4265
|
}
|
|
@@ -3620,8 +4273,8 @@ __export(teardown_exports, {
|
|
|
3620
4273
|
});
|
|
3621
4274
|
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
3622
4275
|
import { parse as parse3, stringify as stringify2 } from "smol-toml";
|
|
3623
|
-
import
|
|
3624
|
-
import { z as
|
|
4276
|
+
import fg6 from "fast-glob";
|
|
4277
|
+
import { z as z18 } from "zod";
|
|
3625
4278
|
async function planTeardown(root, selected) {
|
|
3626
4279
|
const edits = [], diagnostics = [];
|
|
3627
4280
|
const inputs = /* @__PURE__ */ new Map();
|
|
@@ -3641,7 +4294,7 @@ async function planTeardown(root, selected) {
|
|
|
3641
4294
|
const setupBefore = await read(SETUP_PATH);
|
|
3642
4295
|
const setup = setupBefore === null ? null : setupSchema.parse(JSON.parse(setupBefore));
|
|
3643
4296
|
const automationBefore = await read(AUTOMATION_PATH);
|
|
3644
|
-
const automation = automationBefore === null ? null :
|
|
4297
|
+
const automation = automationBefore === null ? null : recordSchema2.parse(JSON.parse(automationBefore));
|
|
3645
4298
|
const commands = (host) => new Set([
|
|
3646
4299
|
automation?.hosts[host]?.command,
|
|
3647
4300
|
...ownership.files[configPath(host)]?.hookCommands ?? [],
|
|
@@ -3755,7 +4408,7 @@ async function planTeardown(root, selected) {
|
|
|
3755
4408
|
if (diagnostics.length) edits.splice(instructionEditStart);
|
|
3756
4409
|
if (!diagnostics.length) {
|
|
3757
4410
|
const cleanup = [];
|
|
3758
|
-
const receiptFiles = await
|
|
4411
|
+
const receiptFiles = await fg6(
|
|
3759
4412
|
selected.map((host) => ".mason/reports/automation/*/setup-" + host + ".json"),
|
|
3760
4413
|
{ cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false }
|
|
3761
4414
|
);
|
|
@@ -3768,7 +4421,7 @@ async function planTeardown(root, selected) {
|
|
|
3768
4421
|
if (!remainingHost) await attempt(".mason/local/project.json", async () => {
|
|
3769
4422
|
const before = await read(".mason/local/project.json");
|
|
3770
4423
|
if (before === null) return;
|
|
3771
|
-
|
|
4424
|
+
z18.object({ version: z18.literal(1), initializedAt: z18.string() }).strict().parse(JSON.parse(before));
|
|
3772
4425
|
cleanup.push({ path: ".mason/local/project.json", before, after: null });
|
|
3773
4426
|
});
|
|
3774
4427
|
if (!diagnostics.length) {
|
|
@@ -3840,14 +4493,14 @@ var init_teardown = __esm({
|
|
|
3840
4493
|
init_model();
|
|
3841
4494
|
init_ownership();
|
|
3842
4495
|
hosts = ["codex", "claude"];
|
|
3843
|
-
object2 =
|
|
4496
|
+
object2 = z18.record(z18.unknown());
|
|
3844
4497
|
json = (value) => JSON.stringify(value, null, 2) + "\n";
|
|
3845
4498
|
}
|
|
3846
4499
|
});
|
|
3847
4500
|
|
|
3848
4501
|
// src/review/cochange.ts
|
|
3849
|
-
import { execFile as
|
|
3850
|
-
import { promisify as
|
|
4502
|
+
import { execFile as execFile12 } from "child_process";
|
|
4503
|
+
import { promisify as promisify12 } from "util";
|
|
3851
4504
|
async function buildMatrix(resolvedRoot) {
|
|
3852
4505
|
try {
|
|
3853
4506
|
const { stdout: shallow } = await exec9("git", ["rev-parse", "--is-shallow-repository"], { cwd: resolvedRoot });
|
|
@@ -3912,7 +4565,7 @@ var exec9, HISTORY_COMMITS, MIN_FILE_COMMITS, MIN_SHARED_COMMITS, MIN_COCHANGE_R
|
|
|
3912
4565
|
var init_cochange = __esm({
|
|
3913
4566
|
"src/review/cochange.ts"() {
|
|
3914
4567
|
"use strict";
|
|
3915
|
-
exec9 =
|
|
4568
|
+
exec9 = promisify12(execFile12);
|
|
3916
4569
|
HISTORY_COMMITS = 1500;
|
|
3917
4570
|
MIN_FILE_COMMITS = 5;
|
|
3918
4571
|
MIN_SHARED_COMMITS = 4;
|
|
@@ -3967,7 +4620,7 @@ var init_types2 = __esm({
|
|
|
3967
4620
|
});
|
|
3968
4621
|
|
|
3969
4622
|
// src/review/evidence/vitest.ts
|
|
3970
|
-
import { z as
|
|
4623
|
+
import { z as z19 } from "zod";
|
|
3971
4624
|
function parseVitest(raw, sourceRoot) {
|
|
3972
4625
|
const report = schema.parse(raw);
|
|
3973
4626
|
const findings = [], diagnostics = [];
|
|
@@ -4010,30 +4663,30 @@ function parseVitest(raw, sourceRoot) {
|
|
|
4010
4663
|
diagnostics
|
|
4011
4664
|
};
|
|
4012
4665
|
}
|
|
4013
|
-
var
|
|
4666
|
+
var count3, schema;
|
|
4014
4667
|
var init_vitest = __esm({
|
|
4015
4668
|
"src/review/evidence/vitest.ts"() {
|
|
4016
4669
|
"use strict";
|
|
4017
4670
|
init_paths2();
|
|
4018
4671
|
init_types2();
|
|
4019
|
-
|
|
4020
|
-
schema =
|
|
4021
|
-
success:
|
|
4022
|
-
numTotalTests:
|
|
4023
|
-
numPassedTests:
|
|
4024
|
-
numFailedTests:
|
|
4025
|
-
numPendingTests:
|
|
4026
|
-
numTodoTests:
|
|
4027
|
-
numFailedTestSuites:
|
|
4028
|
-
testResults:
|
|
4029
|
-
name:
|
|
4030
|
-
status:
|
|
4031
|
-
message:
|
|
4032
|
-
assertionResults:
|
|
4033
|
-
fullName:
|
|
4034
|
-
status:
|
|
4035
|
-
failureMessages:
|
|
4036
|
-
location:
|
|
4672
|
+
count3 = z19.number().int().nonnegative();
|
|
4673
|
+
schema = z19.object({
|
|
4674
|
+
success: z19.boolean(),
|
|
4675
|
+
numTotalTests: count3,
|
|
4676
|
+
numPassedTests: count3,
|
|
4677
|
+
numFailedTests: count3,
|
|
4678
|
+
numPendingTests: count3,
|
|
4679
|
+
numTodoTests: count3,
|
|
4680
|
+
numFailedTestSuites: count3,
|
|
4681
|
+
testResults: z19.array(z19.object({
|
|
4682
|
+
name: z19.string().min(1),
|
|
4683
|
+
status: z19.enum(["passed", "failed"]),
|
|
4684
|
+
message: z19.string().optional(),
|
|
4685
|
+
assertionResults: z19.array(z19.object({
|
|
4686
|
+
fullName: z19.string(),
|
|
4687
|
+
status: z19.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
|
|
4688
|
+
failureMessages: z19.array(z19.string()).nullable().optional(),
|
|
4689
|
+
location: z19.object({ line: count3, column: count3 }).nullable().optional()
|
|
4037
4690
|
}))
|
|
4038
4691
|
}))
|
|
4039
4692
|
});
|
|
@@ -4041,7 +4694,7 @@ var init_vitest = __esm({
|
|
|
4041
4694
|
});
|
|
4042
4695
|
|
|
4043
4696
|
// src/review/evidence/sarif.ts
|
|
4044
|
-
import { z as
|
|
4697
|
+
import { z as z20 } from "zod";
|
|
4045
4698
|
function parseSarif(raw, sourceRoot) {
|
|
4046
4699
|
const report = schema2.parse(raw), findings = [], diagnostics = [];
|
|
4047
4700
|
let active = 0, suppressed = 0, absent = 0, unresolved = 0;
|
|
@@ -4132,40 +4785,98 @@ var init_sarif = __esm({
|
|
|
4132
4785
|
"use strict";
|
|
4133
4786
|
init_paths2();
|
|
4134
4787
|
init_types2();
|
|
4135
|
-
index =
|
|
4136
|
-
artifact =
|
|
4137
|
-
message =
|
|
4138
|
-
level =
|
|
4139
|
-
location =
|
|
4140
|
-
schema2 =
|
|
4141
|
-
version:
|
|
4142
|
-
runs:
|
|
4143
|
-
tool:
|
|
4144
|
-
invocations:
|
|
4145
|
-
versionControlProvenance:
|
|
4146
|
-
originalUriBaseIds:
|
|
4147
|
-
artifacts:
|
|
4148
|
-
results:
|
|
4149
|
-
ruleId:
|
|
4788
|
+
index = z20.number().int().nonnegative();
|
|
4789
|
+
artifact = z20.object({ uri: z20.string().optional(), uriBaseId: z20.string().optional(), index: index.optional() });
|
|
4790
|
+
message = z20.object({ text: z20.string().optional(), markdown: z20.string().optional(), id: z20.string().optional(), arguments: z20.array(z20.string()).optional() });
|
|
4791
|
+
level = z20.enum(["error", "warning", "note", "none"]);
|
|
4792
|
+
location = z20.object({ physicalLocation: z20.object({ artifactLocation: artifact.optional(), region: z20.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
|
|
4793
|
+
schema2 = z20.object({
|
|
4794
|
+
version: z20.literal("2.1.0"),
|
|
4795
|
+
runs: z20.array(z20.object({
|
|
4796
|
+
tool: z20.object({ driver: z20.object({ name: z20.string().min(1), rules: z20.array(z20.object({ id: z20.string(), defaultConfiguration: z20.object({ level: level.optional() }).optional(), messageStrings: z20.record(message).optional() })).optional(), globalMessageStrings: z20.record(message).optional() }) }),
|
|
4797
|
+
invocations: z20.array(z20.object({ executionSuccessful: z20.boolean(), commandLine: z20.string().optional(), toolExecutionNotifications: z20.array(z20.object({ level: level.optional(), message })).optional() })).optional(),
|
|
4798
|
+
versionControlProvenance: z20.array(z20.object({ revisionId: z20.string().optional() })).optional(),
|
|
4799
|
+
originalUriBaseIds: z20.record(artifact).optional(),
|
|
4800
|
+
artifacts: z20.array(z20.object({ location: artifact.optional() })).optional(),
|
|
4801
|
+
results: z20.array(z20.object({
|
|
4802
|
+
ruleId: z20.string().optional(),
|
|
4150
4803
|
ruleIndex: index.optional(),
|
|
4151
4804
|
message,
|
|
4152
4805
|
level: level.optional(),
|
|
4153
|
-
kind:
|
|
4154
|
-
baselineState:
|
|
4155
|
-
suppressions:
|
|
4156
|
-
locations:
|
|
4157
|
-
relatedLocations:
|
|
4806
|
+
kind: z20.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
|
|
4807
|
+
baselineState: z20.enum(["new", "unchanged", "updated", "absent"]).optional(),
|
|
4808
|
+
suppressions: z20.array(z20.object({ kind: z20.enum(["inSource", "external"]), status: z20.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
|
|
4809
|
+
locations: z20.array(location).optional(),
|
|
4810
|
+
relatedLocations: z20.array(location).optional()
|
|
4158
4811
|
})).optional()
|
|
4159
4812
|
}))
|
|
4160
4813
|
});
|
|
4161
4814
|
}
|
|
4162
4815
|
});
|
|
4163
4816
|
|
|
4817
|
+
// src/review/evidence/check.ts
|
|
4818
|
+
import { z as z21 } from "zod";
|
|
4819
|
+
function parseCheck(raw, sourceRoot) {
|
|
4820
|
+
const report = schema3.parse(raw);
|
|
4821
|
+
const diagnostics = [], findings = [];
|
|
4822
|
+
const counts = { total: report.results.length, passed: 0, failed: 0, skipped: 0 };
|
|
4823
|
+
for (const [index2, result] of report.results.entries()) {
|
|
4824
|
+
counts[result.status]++;
|
|
4825
|
+
const locations = result.locations.flatMap((location2) => {
|
|
4826
|
+
const file = evidencePath(location2.file, sourceRoot);
|
|
4827
|
+
if (!file) {
|
|
4828
|
+
diagnostics.push(`Check location is outside the declared checkout or invalid: ${location2.file}`);
|
|
4829
|
+
return [];
|
|
4830
|
+
}
|
|
4831
|
+
return [{ ...location2, file }];
|
|
4832
|
+
});
|
|
4833
|
+
if (result.status === "failed") findings.push({
|
|
4834
|
+
id: String(index2),
|
|
4835
|
+
...messagePreview([result.name, result.message].filter(Boolean).join("\n")),
|
|
4836
|
+
severity: "error",
|
|
4837
|
+
state: "active",
|
|
4838
|
+
locations
|
|
4839
|
+
});
|
|
4840
|
+
}
|
|
4841
|
+
if (!counts.total) diagnostics.push("No check results were recorded; an empty report is not passing evidence.");
|
|
4842
|
+
if (counts.skipped) diagnostics.push(`${counts.skipped} checks were skipped; verification is incomplete.`);
|
|
4843
|
+
return {
|
|
4844
|
+
outcome: counts.failed ? "failed" : !counts.total ? "unavailable" : !counts.passed ? "skipped" : "passed",
|
|
4845
|
+
counts,
|
|
4846
|
+
findings,
|
|
4847
|
+
incomplete: diagnostics.length > 0,
|
|
4848
|
+
diagnostics,
|
|
4849
|
+
...report.commit ? { reportedCommits: [report.commit] } : {}
|
|
4850
|
+
};
|
|
4851
|
+
}
|
|
4852
|
+
var schema3;
|
|
4853
|
+
var init_check = __esm({
|
|
4854
|
+
"src/review/evidence/check.ts"() {
|
|
4855
|
+
"use strict";
|
|
4856
|
+
init_paths2();
|
|
4857
|
+
init_types2();
|
|
4858
|
+
schema3 = z21.object({
|
|
4859
|
+
version: z21.literal(1),
|
|
4860
|
+
commit: z21.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).optional(),
|
|
4861
|
+
results: z21.array(z21.object({
|
|
4862
|
+
name: z21.string().min(1).max(1e3),
|
|
4863
|
+
status: z21.enum(["passed", "failed", "skipped"]),
|
|
4864
|
+
message: z21.string().max(1e5).optional(),
|
|
4865
|
+
locations: z21.array(z21.object({
|
|
4866
|
+
file: z21.string().min(1).max(4e3),
|
|
4867
|
+
line: z21.number().int().positive().optional(),
|
|
4868
|
+
column: z21.number().int().positive().optional()
|
|
4869
|
+
})).max(100).default([])
|
|
4870
|
+
})).max(1e5)
|
|
4871
|
+
});
|
|
4872
|
+
}
|
|
4873
|
+
});
|
|
4874
|
+
|
|
4164
4875
|
// src/review/evidence.ts
|
|
4165
|
-
import
|
|
4166
|
-
import { z as
|
|
4876
|
+
import path22 from "path";
|
|
4877
|
+
import { z as z22 } from "zod";
|
|
4167
4878
|
function relativeArtifact(root, file) {
|
|
4168
|
-
const relative =
|
|
4879
|
+
const relative = path22.isAbsolute(file) ? path22.relative(root, file) : file;
|
|
4169
4880
|
const normalized = normalizeRepoPath(relative);
|
|
4170
4881
|
if (!normalized) throw new Error(`Evidence artifact must be inside the repository: ${file}`);
|
|
4171
4882
|
return normalized;
|
|
@@ -4204,7 +4915,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
4204
4915
|
let manifest, raw;
|
|
4205
4916
|
try {
|
|
4206
4917
|
manifest = relativeArtifact(root, supplied);
|
|
4207
|
-
raw =
|
|
4918
|
+
raw = z22.object({ version: z22.literal(1), checks: z22.array(z22.unknown()) }).parse(await readStoreJson(root, manifest));
|
|
4208
4919
|
} catch (error) {
|
|
4209
4920
|
output.diagnostics.push(`${supplied}: ${String(error)}`);
|
|
4210
4921
|
continue;
|
|
@@ -4243,7 +4954,8 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
4243
4954
|
counts: {},
|
|
4244
4955
|
incomplete: false,
|
|
4245
4956
|
diagnostics: [],
|
|
4246
|
-
truncated: false
|
|
4957
|
+
truncated: false,
|
|
4958
|
+
...check.environment ? { environment: check.environment } : {}
|
|
4247
4959
|
};
|
|
4248
4960
|
output.checks.push(result);
|
|
4249
4961
|
if (check.workingTreeClean !== true) result.diagnostics.push("The check's working tree was dirty or not recorded; its results cannot be attributed to the claimed commit alone.");
|
|
@@ -4256,13 +4968,13 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
4256
4968
|
}
|
|
4257
4969
|
try {
|
|
4258
4970
|
if (!check.report) throw new Error("Completed check has no report artifact.");
|
|
4259
|
-
if (check.kind === "tests" !== (check.report.format === "vitest-json")) throw new Error("Test checks require vitest-json; analysis checks require sarif.");
|
|
4971
|
+
if (check.report.format !== "mason-check-json" && check.kind === "tests" !== (check.report.format === "vitest-json")) throw new Error("Test checks require vitest-json or mason-check-json; analysis checks require sarif or mason-check-json.");
|
|
4260
4972
|
const file = relativeArtifact(root, check.report.path);
|
|
4261
4973
|
const report = await readStoreJson(root, file);
|
|
4262
4974
|
if (report === null) throw new Error(`Report artifact is missing: ${file}`);
|
|
4263
4975
|
const sourceRoot = check.sourceRoot ?? root;
|
|
4264
|
-
if (!
|
|
4265
|
-
const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : parseSarif(report, sourceRoot);
|
|
4976
|
+
if (!path22.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
|
|
4977
|
+
const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : check.report.format === "mason-check-json" ? parseCheck(report, sourceRoot) : parseSarif(report, sourceRoot);
|
|
4266
4978
|
result.outcome = parsed.outcome;
|
|
4267
4979
|
result.counts = parsed.counts;
|
|
4268
4980
|
result.incomplete = parsed.incomplete;
|
|
@@ -4282,7 +4994,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
4282
4994
|
result.outcome = "failed";
|
|
4283
4995
|
result.diagnostics.push(`Check command exited ${check.exitCode} despite a report with no active failures.`);
|
|
4284
4996
|
}
|
|
4285
|
-
if (check.
|
|
4997
|
+
if (check.kind === "tests" && pairs === void 0) {
|
|
4286
4998
|
try {
|
|
4287
4999
|
pairs = (await buildTestMap(root)).paired;
|
|
4288
5000
|
} catch (error) {
|
|
@@ -4356,29 +5068,36 @@ var init_evidence2 = __esm({
|
|
|
4356
5068
|
init_provenance();
|
|
4357
5069
|
init_vitest();
|
|
4358
5070
|
init_sarif();
|
|
5071
|
+
init_check();
|
|
4359
5072
|
init_types2();
|
|
4360
|
-
checkSchema2 =
|
|
4361
|
-
id:
|
|
4362
|
-
kind:
|
|
4363
|
-
tool:
|
|
4364
|
-
command:
|
|
4365
|
-
commit:
|
|
4366
|
-
workingTreeClean:
|
|
4367
|
-
source:
|
|
4368
|
-
sourceRoot:
|
|
4369
|
-
status:
|
|
4370
|
-
reason:
|
|
4371
|
-
exitCode:
|
|
4372
|
-
|
|
5073
|
+
checkSchema2 = z22.object({
|
|
5074
|
+
id: z22.string().min(1).max(100),
|
|
5075
|
+
kind: z22.enum(["tests", "static-analysis", "security", "complexity", "duplication", "documentation"]),
|
|
5076
|
+
tool: z22.string().min(1).max(200),
|
|
5077
|
+
command: z22.string().min(1).max(2e3),
|
|
5078
|
+
commit: z22.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
|
|
5079
|
+
workingTreeClean: z22.boolean().optional(),
|
|
5080
|
+
source: z22.string().max(2e3).optional(),
|
|
5081
|
+
sourceRoot: z22.string().min(1).max(4e3).optional(),
|
|
5082
|
+
status: z22.enum(["completed", "skipped", "unavailable"]).default("completed"),
|
|
5083
|
+
reason: z22.string().min(1).max(2e3).optional(),
|
|
5084
|
+
exitCode: z22.number().int().nullable().optional(),
|
|
5085
|
+
environment: z22.object({
|
|
5086
|
+
toolVersion: z22.string().min(1).max(200),
|
|
5087
|
+
runtime: z22.string().min(1).max(200).optional(),
|
|
5088
|
+
platform: z22.string().min(1).max(200).optional(),
|
|
5089
|
+
configuration: z22.array(z22.string().min(1).max(4e3)).max(100).optional()
|
|
5090
|
+
}).optional(),
|
|
5091
|
+
report: z22.object({ format: z22.enum(["vitest-json", "sarif", "mason-check-json"]), path: z22.string().min(1).max(4e3) }).optional()
|
|
4373
5092
|
});
|
|
4374
5093
|
}
|
|
4375
5094
|
});
|
|
4376
5095
|
|
|
4377
5096
|
// src/review/review.ts
|
|
4378
|
-
import
|
|
4379
|
-
import
|
|
4380
|
-
import { execFile as
|
|
4381
|
-
import { promisify as
|
|
5097
|
+
import fs18 from "fs/promises";
|
|
5098
|
+
import path23 from "path";
|
|
5099
|
+
import { execFile as execFile13 } from "child_process";
|
|
5100
|
+
import { promisify as promisify13 } from "util";
|
|
4382
5101
|
async function resolveMergeBase(resolvedRoot, base, head) {
|
|
4383
5102
|
try {
|
|
4384
5103
|
const { stdout } = await exec10("git", ["merge-base", base, head], {
|
|
@@ -4405,7 +5124,7 @@ function anchorsTouched(record, changedFiles) {
|
|
|
4405
5124
|
return matchingPaths(record.files, changedFiles);
|
|
4406
5125
|
}
|
|
4407
5126
|
async function computeReview(rootDir, base, options = {}) {
|
|
4408
|
-
const resolvedRoot =
|
|
5127
|
+
const resolvedRoot = path23.resolve(rootDir);
|
|
4409
5128
|
const head = await getCurrentGitHash(resolvedRoot);
|
|
4410
5129
|
if (head === "unknown") return null;
|
|
4411
5130
|
const mergeBase = await resolveMergeBase(resolvedRoot, base, head);
|
|
@@ -4455,7 +5174,7 @@ async function computeReview(rootDir, base, options = {}) {
|
|
|
4455
5174
|
analyzed,
|
|
4456
5175
|
async (relPath) => {
|
|
4457
5176
|
try {
|
|
4458
|
-
await
|
|
5177
|
+
await fs18.access(path23.join(resolvedRoot, relPath));
|
|
4459
5178
|
return true;
|
|
4460
5179
|
} catch {
|
|
4461
5180
|
return false;
|
|
@@ -4500,7 +5219,7 @@ var init_review = __esm({
|
|
|
4500
5219
|
init_cochange();
|
|
4501
5220
|
init_evidence2();
|
|
4502
5221
|
init_snapshot();
|
|
4503
|
-
exec10 =
|
|
5222
|
+
exec10 = promisify13(execFile13);
|
|
4504
5223
|
MAX_ANALYZED_FILES = 50;
|
|
4505
5224
|
}
|
|
4506
5225
|
});
|
|
@@ -4509,7 +5228,7 @@ var init_review = __esm({
|
|
|
4509
5228
|
async function auditSummary(root) {
|
|
4510
5229
|
try {
|
|
4511
5230
|
const report = await computeAudit(root);
|
|
4512
|
-
if (!report) return { status: "no-context-files", reason: "No
|
|
5231
|
+
if (!report) return { status: "no-context-files", reason: "No README.md or agent instruction files found to audit." };
|
|
4513
5232
|
if (!report.gitAvailable) return { status: "unavailable", reason: "Audit needs readable Git history to verify documentation claims.", docs: report.docs };
|
|
4514
5233
|
return {
|
|
4515
5234
|
...report,
|