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.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 quote2 = "", 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 (quote2) {
|
|
361
|
+
if (char === quote2) quote2 = "";
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (char === "'" || char === '"') {
|
|
365
|
+
quote2 = 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",
|
|
@@ -927,67 +808,502 @@ async function commitsTouchingSince(resolvedRoot, fromHash, pathspecs) {
|
|
|
927
808
|
}
|
|
928
809
|
return { commits, total: commits.length };
|
|
929
810
|
} catch {
|
|
930
|
-
return null;
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
var exec2, COMMIT_FORMAT;
|
|
815
|
+
var init_git = __esm({
|
|
816
|
+
"src/audit/git.ts"() {
|
|
817
|
+
"use strict";
|
|
818
|
+
exec2 = promisify2(execFile2);
|
|
819
|
+
COMMIT_FORMAT = "%H%x09%cI%x09%s";
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
// src/audit/inputs.ts
|
|
824
|
+
import fs4 from "fs/promises";
|
|
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);
|
|
832
|
+
try {
|
|
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 });
|
|
837
|
+
}
|
|
838
|
+
throw error;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
async function readAuditInput(root, file) {
|
|
842
|
+
try {
|
|
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;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
async function auditGlob(root, patterns, options) {
|
|
852
|
+
if (Array.isArray(patterns) && !patterns.length) return [];
|
|
853
|
+
const directories = /* @__PURE__ */ new Set();
|
|
854
|
+
const readdir2 = ((directory, opts, callback) => {
|
|
855
|
+
const relative = path5.relative(root, directory).split(path5.sep).join("/") || ".";
|
|
856
|
+
directories.add(relative);
|
|
857
|
+
if (directories.size > MAX_INPUTS) {
|
|
858
|
+
callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
auditInputPath(root, relative).then(() => {
|
|
862
|
+
nativeFs.readdir(directory, opts, callback);
|
|
863
|
+
}, callback);
|
|
864
|
+
});
|
|
865
|
+
const lstat2 = ((file, callback) => {
|
|
866
|
+
const relative = path5.relative(root, file).split(path5.sep).join("/") || ".";
|
|
867
|
+
auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
|
|
868
|
+
});
|
|
869
|
+
const stream = fg2.stream(patterns, {
|
|
870
|
+
cwd: root,
|
|
871
|
+
ignore: options.ignore,
|
|
872
|
+
onlyFiles: false,
|
|
873
|
+
onlyDirectories: options.onlyDirectories,
|
|
874
|
+
objectMode: true,
|
|
875
|
+
followSymbolicLinks: options.followSymbolicLinks ?? true,
|
|
876
|
+
fs: { readdir: readdir2, lstat: lstat2 },
|
|
877
|
+
concurrency: 16
|
|
878
|
+
});
|
|
879
|
+
const matches = [];
|
|
880
|
+
try {
|
|
881
|
+
for await (const value of stream) {
|
|
882
|
+
const entry = value;
|
|
883
|
+
const file = entry.path;
|
|
884
|
+
if (options.select && !options.select(file)) continue;
|
|
885
|
+
await auditInputPath(root, file);
|
|
886
|
+
if (!options.onlyDirectories && !entry.dirent.isFile()) continue;
|
|
887
|
+
matches.push(file);
|
|
888
|
+
if (matches.length > MAX_INPUTS) {
|
|
889
|
+
const largest = [...matches.reduce((counts, item) => {
|
|
890
|
+
const dir = item.split("/")[0];
|
|
891
|
+
counts.set(dir, (counts.get(dir) ?? 0) + 1);
|
|
892
|
+
return counts;
|
|
893
|
+
}, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
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.`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
} finally {
|
|
898
|
+
stream.destroy();
|
|
899
|
+
}
|
|
900
|
+
return matches.sort();
|
|
901
|
+
}
|
|
902
|
+
async function gitSourcePaths(root) {
|
|
903
|
+
const { stdout } = await execute(
|
|
904
|
+
"git",
|
|
905
|
+
[
|
|
906
|
+
"ls-files",
|
|
907
|
+
"-z",
|
|
908
|
+
"--cached",
|
|
909
|
+
"--others",
|
|
910
|
+
"--exclude-standard",
|
|
911
|
+
"--",
|
|
912
|
+
...SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`)
|
|
913
|
+
],
|
|
914
|
+
{ cwd: root, maxBuffer: 32 * 1024 * 1024, timeout: 1e4 }
|
|
915
|
+
);
|
|
916
|
+
return [...new Set(stdout.split("\0").filter(Boolean))].sort();
|
|
917
|
+
}
|
|
918
|
+
async function gitIgnoredPaths(root, files) {
|
|
919
|
+
if (!files.length) return /* @__PURE__ */ new Set();
|
|
920
|
+
const operation = execute(
|
|
921
|
+
"git",
|
|
922
|
+
["check-ignore", "-z", "--stdin"],
|
|
923
|
+
{ cwd: root, maxBuffer: 4 * 1024 * 1024, timeout: 1e4 }
|
|
924
|
+
);
|
|
925
|
+
operation.child.stdin.on("error", () => {
|
|
926
|
+
});
|
|
927
|
+
operation.child.stdin.end(files.join("\0") + "\0");
|
|
928
|
+
try {
|
|
929
|
+
return new Set((await operation).stdout.split("\0").filter(Boolean));
|
|
930
|
+
} catch (error) {
|
|
931
|
+
if (error.code === 1) return /* @__PURE__ */ new Set();
|
|
932
|
+
throw error;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
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";
|
|
931
1218
|
}
|
|
932
1219
|
}
|
|
933
|
-
var exec4,
|
|
934
|
-
var
|
|
935
|
-
"src/
|
|
1220
|
+
var exec4, repoPath, verificationFields, featureSchema, flowSchema, snapshotSchema;
|
|
1221
|
+
var init_snapshot = __esm({
|
|
1222
|
+
"src/snapshot/snapshot.ts"() {
|
|
936
1223
|
"use strict";
|
|
937
|
-
|
|
938
|
-
|
|
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();
|
|
939
1254
|
}
|
|
940
1255
|
});
|
|
941
1256
|
|
|
942
|
-
// src/
|
|
943
|
-
import
|
|
944
|
-
import
|
|
945
|
-
import { execFile as
|
|
946
|
-
import { promisify as
|
|
947
|
-
|
|
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;
|
|
948
1281
|
try {
|
|
949
|
-
const { stdout } = await exec5(
|
|
950
|
-
|
|
951
|
-
["status", "--porcelain", "--", relPath],
|
|
952
|
-
{ cwd: resolvedRoot }
|
|
953
|
-
);
|
|
954
|
-
return stdout.trim().length > 0;
|
|
1282
|
+
const { stdout } = await exec5("git", ["diff", "--name-status", "-z", "-M", fromHash, toHash, "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 });
|
|
1283
|
+
return parseChanges(stdout);
|
|
955
1284
|
} catch {
|
|
956
|
-
return
|
|
1285
|
+
return null;
|
|
957
1286
|
}
|
|
958
1287
|
}
|
|
959
|
-
async function
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
return {
|
|
969
|
-
|
|
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);
|
|
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
|
+
}
|
|
978
1299
|
}
|
|
979
|
-
var exec5
|
|
980
|
-
var
|
|
981
|
-
"src/
|
|
1300
|
+
var exec5;
|
|
1301
|
+
var init_drift = __esm({
|
|
1302
|
+
"src/drift/drift.ts"() {
|
|
982
1303
|
"use strict";
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
exec5 =
|
|
986
|
-
DOC_CANDIDATES = [
|
|
987
|
-
"AGENTS.md",
|
|
988
|
-
"CLAUDE.md",
|
|
989
|
-
".claude/CLAUDE.md"
|
|
990
|
-
];
|
|
1304
|
+
init_snapshot();
|
|
1305
|
+
init_paths();
|
|
1306
|
+
exec5 = promisify6(execFile6);
|
|
991
1307
|
}
|
|
992
1308
|
});
|
|
993
1309
|
|
|
@@ -1008,91 +1324,51 @@ var init_types = __esm({
|
|
|
1008
1324
|
});
|
|
1009
1325
|
|
|
1010
1326
|
// src/audit/checks/deleted-reference.ts
|
|
1011
|
-
import
|
|
1012
|
-
import path8 from "path";
|
|
1013
|
-
async function exists(absPath) {
|
|
1014
|
-
try {
|
|
1015
|
-
await fs5.access(absPath);
|
|
1016
|
-
return true;
|
|
1017
|
-
} catch {
|
|
1018
|
-
return false;
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1327
|
+
import path11 from "path";
|
|
1021
1328
|
async function checkDeletedReferences(ctx) {
|
|
1022
1329
|
const result = emptyResult();
|
|
1023
1330
|
for (const doc of ctx.docs) {
|
|
1024
1331
|
const changes = ctx.changesSinceDoc.get(doc.path);
|
|
1025
|
-
const renames =
|
|
1026
|
-
for (const change of changes ?? []) {
|
|
1027
|
-
if (change.status === "renamed" && change.previousPath) {
|
|
1028
|
-
renames.set(change.previousPath, change.path);
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1332
|
+
const renames = new Map((changes ?? []).filter((c) => c.status === "renamed" && c.previousPath).map((c) => [c.previousPath, c.path]));
|
|
1031
1333
|
for (const claim of doc.claims.paths) {
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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
|
-
}
|
|
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.`
|
|
1074
1340
|
});
|
|
1075
1341
|
continue;
|
|
1076
1342
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
)
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
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
|
|
1094
1364
|
};
|
|
1095
|
-
|
|
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
|
+
}
|
|
1096
1372
|
}
|
|
1097
1373
|
}
|
|
1098
1374
|
return result;
|
|
@@ -1100,138 +1376,16 @@ async function checkDeletedReferences(ctx) {
|
|
|
1100
1376
|
var init_deleted_reference = __esm({
|
|
1101
1377
|
"src/audit/checks/deleted-reference.ts"() {
|
|
1102
1378
|
"use strict";
|
|
1103
|
-
init_git();
|
|
1104
|
-
|
|
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;
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
async function auditGlob(root, patterns, options) {
|
|
1137
|
-
if (Array.isArray(patterns) && !patterns.length) return [];
|
|
1138
|
-
const directories = /* @__PURE__ */ new Set();
|
|
1139
|
-
const readdir2 = ((directory, opts, callback) => {
|
|
1140
|
-
const relative = path9.relative(root, directory).split(path9.sep).join("/") || ".";
|
|
1141
|
-
directories.add(relative);
|
|
1142
|
-
if (directories.size > MAX_INPUTS) {
|
|
1143
|
-
callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
|
|
1144
|
-
return;
|
|
1145
|
-
}
|
|
1146
|
-
auditInputPath(root, relative).then(() => {
|
|
1147
|
-
nativeFs.readdir(directory, opts, callback);
|
|
1148
|
-
}, callback);
|
|
1149
|
-
});
|
|
1150
|
-
const lstat2 = ((file, callback) => {
|
|
1151
|
-
const relative = path9.relative(root, file).split(path9.sep).join("/") || ".";
|
|
1152
|
-
auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
|
|
1153
|
-
});
|
|
1154
|
-
const stream = fg2.stream(patterns, {
|
|
1155
|
-
cwd: root,
|
|
1156
|
-
ignore: options.ignore,
|
|
1157
|
-
onlyFiles: false,
|
|
1158
|
-
onlyDirectories: options.onlyDirectories,
|
|
1159
|
-
objectMode: true,
|
|
1160
|
-
followSymbolicLinks: options.followSymbolicLinks ?? true,
|
|
1161
|
-
fs: { readdir: readdir2, lstat: lstat2 },
|
|
1162
|
-
concurrency: 16
|
|
1163
|
-
});
|
|
1164
|
-
const matches = [];
|
|
1165
|
-
try {
|
|
1166
|
-
for await (const value of stream) {
|
|
1167
|
-
const entry = value;
|
|
1168
|
-
const file = entry.path;
|
|
1169
|
-
if (options.select && !options.select(file)) continue;
|
|
1170
|
-
await auditInputPath(root, file);
|
|
1171
|
-
if (!options.onlyDirectories && !entry.dirent.isFile()) continue;
|
|
1172
|
-
matches.push(file);
|
|
1173
|
-
if (matches.length > MAX_INPUTS) {
|
|
1174
|
-
const largest = [...matches.reduce((counts, item) => {
|
|
1175
|
-
const dir = item.split("/")[0];
|
|
1176
|
-
counts.set(dir, (counts.get(dir) ?? 0) + 1);
|
|
1177
|
-
return counts;
|
|
1178
|
-
}, /* @__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, count3]) => `${dir} (${count3})`).join(", ")}. Narrow this check's inputs.`);
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
} finally {
|
|
1183
|
-
stream.destroy();
|
|
1184
|
-
}
|
|
1185
|
-
return matches.sort();
|
|
1186
|
-
}
|
|
1187
|
-
async function gitSourcePaths(root) {
|
|
1188
|
-
const { stdout } = await execute(
|
|
1189
|
-
"git",
|
|
1190
|
-
[
|
|
1191
|
-
"ls-files",
|
|
1192
|
-
"-z",
|
|
1193
|
-
"--cached",
|
|
1194
|
-
"--others",
|
|
1195
|
-
"--exclude-standard",
|
|
1196
|
-
"--",
|
|
1197
|
-
...SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`)
|
|
1198
|
-
],
|
|
1199
|
-
{ cwd: root, maxBuffer: 32 * 1024 * 1024, timeout: 1e4 }
|
|
1200
|
-
);
|
|
1201
|
-
return [...new Set(stdout.split("\0").filter(Boolean))].sort();
|
|
1202
|
-
}
|
|
1203
|
-
async function gitIgnoredPaths(root, files) {
|
|
1204
|
-
if (!files.length) return /* @__PURE__ */ new Set();
|
|
1205
|
-
const operation = execute(
|
|
1206
|
-
"git",
|
|
1207
|
-
["check-ignore", "-z", "--stdin"],
|
|
1208
|
-
{ cwd: root, maxBuffer: 4 * 1024 * 1024, timeout: 1e4 }
|
|
1209
|
-
);
|
|
1210
|
-
operation.child.stdin.on("error", () => {
|
|
1211
|
-
});
|
|
1212
|
-
operation.child.stdin.end(files.join("\0") + "\0");
|
|
1213
|
-
try {
|
|
1214
|
-
return new Set((await operation).stdout.split("\0").filter(Boolean));
|
|
1215
|
-
} catch (error) {
|
|
1216
|
-
if (error.code === 1) return /* @__PURE__ */ new Set();
|
|
1217
|
-
throw error;
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
var execute, MAX_INPUTS;
|
|
1221
|
-
var init_inputs = __esm({
|
|
1222
|
-
"src/audit/inputs.ts"() {
|
|
1223
|
-
"use strict";
|
|
1224
|
-
init_files();
|
|
1225
|
-
init_storage();
|
|
1226
|
-
execute = promisify6(execFile6);
|
|
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, args2) {
|
|
|
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, ...args2) => (await execute2("git", args2, { 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
|
|
2871
|
+
import fs13 from "fs/promises";
|
|
2872
|
+
import path20 from "path";
|
|
2395
2873
|
import { createHash as createHash3 } from "crypto";
|
|
2396
|
-
import { execFile as
|
|
2397
|
-
import { promisify as
|
|
2398
|
-
import
|
|
2399
|
-
import { z as
|
|
2400
|
-
async function
|
|
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, ...args2) {
|
|
2401
2879
|
return (await exec7("git", args2, { 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
|
-
|
|
2999
|
+
cacheSchema = z8.object({ version: z8.literal(1), entries: z8.record(z8.object({ key: z8.string(), result: checkResultSchema })), digest: z8.string() });
|
|
3000
|
+
}
|
|
3001
|
+
});
|
|
3002
|
+
|
|
3003
|
+
// src/automation/execution.ts
|
|
3004
|
+
import os2 from "os";
|
|
3005
|
+
import fs14 from "fs/promises";
|
|
3006
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
3007
|
+
import { z as z9 } from "zod";
|
|
3008
|
+
function parseExecution(raw) {
|
|
3009
|
+
try {
|
|
3010
|
+
return executionSchema.parse(raw);
|
|
3011
|
+
} catch (error) {
|
|
3012
|
+
throw new Error("Invalid automation execution store; receipt history was retained.", { cause: error });
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
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;
|
|
2529
3024
|
}
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
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);
|
|
2537
3039
|
try {
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
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);
|
|
2541
3061
|
}
|
|
2542
3062
|
}
|
|
2543
|
-
async function
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
while (!handle) {
|
|
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,27 +3533,27 @@ 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
|
});
|
|
@@ -2922,16 +3568,16 @@ __export(launcher_exports, {
|
|
|
2922
3568
|
mcpCommand: () => mcpCommand,
|
|
2923
3569
|
prepareLaunch: () => prepareLaunch
|
|
2924
3570
|
});
|
|
2925
|
-
import { execFile as
|
|
2926
|
-
import { promisify as
|
|
2927
|
-
import
|
|
3571
|
+
import { execFile as execFile10 } from "child_process";
|
|
3572
|
+
import { promisify as promisify10 } from "util";
|
|
3573
|
+
import fs15 from "fs/promises";
|
|
2928
3574
|
function globalCommand(args2, platform = process.platform) {
|
|
2929
3575
|
return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args2] } : { command: "mason", args: args2 };
|
|
2930
3576
|
}
|
|
2931
3577
|
async function installedCommand() {
|
|
2932
3578
|
const invocation = globalCommand(["internal-integration-version"]);
|
|
2933
3579
|
try {
|
|
2934
|
-
const { stdout } = await
|
|
3580
|
+
const { stdout } = await promisify10(execFile10)(
|
|
2935
3581
|
invocation.command,
|
|
2936
3582
|
invocation.args,
|
|
2937
3583
|
{ timeout: 5e3, maxBuffer: 8192, windowsHide: true }
|
|
@@ -2944,7 +3590,7 @@ async function installedCommand() {
|
|
|
2944
3590
|
}
|
|
2945
3591
|
}
|
|
2946
3592
|
async function prepareLaunch(host, options = {}) {
|
|
2947
|
-
const root = await
|
|
3593
|
+
const root = await fs15.realpath((await git3(process.cwd(), "rev-parse", "--show-toplevel")).trim());
|
|
2948
3594
|
const entry = (await loadSetup(root))?.hosts[host];
|
|
2949
3595
|
if (!entry) {
|
|
2950
3596
|
if (options.allowInactive) return false;
|
|
@@ -2961,7 +3607,7 @@ var init_launcher = __esm({
|
|
|
2961
3607
|
"use strict";
|
|
2962
3608
|
init_evidence();
|
|
2963
3609
|
init_model();
|
|
2964
|
-
executingVersion = () => true ? "0.
|
|
3610
|
+
executingVersion = () => true ? "0.17.0" : "development";
|
|
2965
3611
|
mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
|
|
2966
3612
|
hookCommand = (host) => `mason --setup-host ${host} auto`;
|
|
2967
3613
|
}
|
|
@@ -2974,8 +3620,8 @@ __export(observations_exports, {
|
|
|
2974
3620
|
observeActivation: () => observeActivation,
|
|
2975
3621
|
readObservation: () => readObservation
|
|
2976
3622
|
});
|
|
2977
|
-
import
|
|
2978
|
-
import { z as
|
|
3623
|
+
import fs16 from "fs/promises";
|
|
3624
|
+
import { z as z12 } from "zod";
|
|
2979
3625
|
function observationPath(directory, host, revision, version = executingVersion()) {
|
|
2980
3626
|
return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
|
|
2981
3627
|
}
|
|
@@ -2991,8 +3637,8 @@ async function observeActivation(dir, event, options = {}) {
|
|
|
2991
3637
|
if (host !== "codex" && host !== "claude" || !revision || !process.env.MASON_SETUP_ROOT) return null;
|
|
2992
3638
|
try {
|
|
2993
3639
|
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];
|
|
2994
|
-
const ws = capturedDirectory ? { root: await
|
|
2995
|
-
if (ws.root !== await
|
|
3640
|
+
const ws = capturedDirectory ? { root: await fs16.realpath(dir), directory: capturedDirectory } : await workspace(dir);
|
|
3641
|
+
if (ws.root !== await fs16.realpath(process.env.MASON_SETUP_ROOT)) return null;
|
|
2996
3642
|
const setup = await loadSetup(ws.root);
|
|
2997
3643
|
if (setup?.hosts[host]?.revision !== revision) return "Mason setup changed; restart the assistant to observe the current integration.";
|
|
2998
3644
|
const directory = ws.directory + "/activation";
|
|
@@ -3040,26 +3686,26 @@ var init_observations = __esm({
|
|
|
3040
3686
|
init_storage();
|
|
3041
3687
|
init_model();
|
|
3042
3688
|
init_launcher();
|
|
3043
|
-
observationSchema =
|
|
3044
|
-
version:
|
|
3045
|
-
masonVersion:
|
|
3046
|
-
root:
|
|
3047
|
-
revision:
|
|
3048
|
-
host:
|
|
3049
|
-
contextCalls:
|
|
3050
|
-
lastContextAt:
|
|
3051
|
-
sessions:
|
|
3052
|
-
events:
|
|
3053
|
-
at:
|
|
3054
|
-
verificationStatus:
|
|
3055
|
-
reportPath:
|
|
3689
|
+
observationSchema = z12.object({
|
|
3690
|
+
version: z12.literal(1),
|
|
3691
|
+
masonVersion: z12.string(),
|
|
3692
|
+
root: z12.string(),
|
|
3693
|
+
revision: z12.string(),
|
|
3694
|
+
host: z12.enum(["codex", "claude"]),
|
|
3695
|
+
contextCalls: z12.number().int().nonnegative(),
|
|
3696
|
+
lastContextAt: z12.string().optional(),
|
|
3697
|
+
sessions: z12.record(z12.object({
|
|
3698
|
+
events: z12.array(z12.enum(events)),
|
|
3699
|
+
at: z12.string(),
|
|
3700
|
+
verificationStatus: z12.string().optional(),
|
|
3701
|
+
reportPath: z12.string().optional()
|
|
3056
3702
|
}))
|
|
3057
3703
|
});
|
|
3058
3704
|
}
|
|
3059
3705
|
});
|
|
3060
3706
|
|
|
3061
3707
|
// src/automation/adapters.ts
|
|
3062
|
-
import { z as
|
|
3708
|
+
import { z as z13 } from "zod";
|
|
3063
3709
|
function normalizeHook(host, raw) {
|
|
3064
3710
|
hostSchema.parse(host);
|
|
3065
3711
|
const input = inputSchema.parse(raw);
|
|
@@ -3075,6 +3721,7 @@ function normalizeHook(host, raw) {
|
|
|
3075
3721
|
}
|
|
3076
3722
|
async function runAutomationHook(host, stdin) {
|
|
3077
3723
|
let name = "";
|
|
3724
|
+
let hook;
|
|
3078
3725
|
try {
|
|
3079
3726
|
if (Buffer.byteLength(stdin) > 1024 * 1024) throw new Error("Hook input exceeds 1 MiB.");
|
|
3080
3727
|
let raw;
|
|
@@ -3084,6 +3731,7 @@ async function runAutomationHook(host, stdin) {
|
|
|
3084
3731
|
throw new Error("Hook input is not valid JSON.");
|
|
3085
3732
|
}
|
|
3086
3733
|
const input = normalizeHook(host, raw);
|
|
3734
|
+
hook = input;
|
|
3087
3735
|
name = input.name;
|
|
3088
3736
|
if (["before_tool", "after_tool"].includes(input.event.event) && input.readOnly) {
|
|
3089
3737
|
const observed = await observeReadOnlyTool(input.cwd, input.event);
|
|
@@ -3105,7 +3753,8 @@ async function runAutomationHook(host, stdin) {
|
|
|
3105
3753
|
}
|
|
3106
3754
|
return { hookSpecificOutput: { hookEventName: name, additionalContext: result.message } };
|
|
3107
3755
|
} catch (error) {
|
|
3108
|
-
const message2 = failureMessage(error);
|
|
3756
|
+
const message2 = hook ? await hookFailureMessage(hook.cwd, host, hook.event.sessionId, error) : failureMessage(error);
|
|
3757
|
+
if (!message2) return null;
|
|
3109
3758
|
return { systemMessage: message2, ...["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse"].includes(name) ? { hookSpecificOutput: { hookEventName: name, additionalContext: message2 } } : {} };
|
|
3110
3759
|
}
|
|
3111
3760
|
}
|
|
@@ -3130,15 +3779,15 @@ var init_adapters = __esm({
|
|
|
3130
3779
|
init_execution();
|
|
3131
3780
|
init_runtime();
|
|
3132
3781
|
init_store();
|
|
3133
|
-
inputSchema =
|
|
3134
|
-
cwd:
|
|
3135
|
-
session_id:
|
|
3136
|
-
hook_event_name:
|
|
3137
|
-
tool_name:
|
|
3138
|
-
tool_use_id:
|
|
3139
|
-
tool_input:
|
|
3140
|
-
stop_hook_active:
|
|
3141
|
-
permission_mode:
|
|
3782
|
+
inputSchema = z13.object({
|
|
3783
|
+
cwd: z13.string().min(1),
|
|
3784
|
+
session_id: z13.string().min(1).max(500),
|
|
3785
|
+
hook_event_name: z13.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"]),
|
|
3786
|
+
tool_name: z13.string().optional(),
|
|
3787
|
+
tool_use_id: z13.string().max(500).optional(),
|
|
3788
|
+
tool_input: z13.unknown().optional(),
|
|
3789
|
+
stop_hook_active: z13.boolean().optional(),
|
|
3790
|
+
permission_mode: z13.string().optional()
|
|
3142
3791
|
});
|
|
3143
3792
|
lifecycle = {
|
|
3144
3793
|
SessionStart: "session_start",
|
|
@@ -3158,8 +3807,8 @@ __export(files_exports, {
|
|
|
3158
3807
|
managedBlock: () => managedBlock,
|
|
3159
3808
|
readText: () => readText
|
|
3160
3809
|
});
|
|
3161
|
-
import
|
|
3162
|
-
import
|
|
3810
|
+
import fs17 from "fs/promises";
|
|
3811
|
+
import path21 from "path";
|
|
3163
3812
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
3164
3813
|
async function readText(root, file) {
|
|
3165
3814
|
try {
|
|
@@ -3188,14 +3837,14 @@ async function applyEdit(root, edit) {
|
|
|
3188
3837
|
if (edit.after === null) {
|
|
3189
3838
|
const file2 = await storePath(root, edit.path);
|
|
3190
3839
|
if (await readText(root, edit.path) !== edit.before) throw new Error("Input changed during removal: " + edit.path);
|
|
3191
|
-
await
|
|
3840
|
+
await fs17.unlink(file2);
|
|
3192
3841
|
return true;
|
|
3193
3842
|
}
|
|
3194
3843
|
const file = await storePath(root, edit.path, true);
|
|
3195
|
-
const temporary =
|
|
3844
|
+
const temporary = path21.join(path21.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
|
|
3196
3845
|
try {
|
|
3197
|
-
const mode = await
|
|
3198
|
-
const handle = await
|
|
3846
|
+
const mode = await fs17.stat(file).then((s) => s.mode & 511, () => 384);
|
|
3847
|
+
const handle = await fs17.open(temporary, "wx", mode);
|
|
3199
3848
|
try {
|
|
3200
3849
|
await handle.writeFile(edit.after, "utf8");
|
|
3201
3850
|
await handle.sync();
|
|
@@ -3203,10 +3852,10 @@ async function applyEdit(root, edit) {
|
|
|
3203
3852
|
await handle.close();
|
|
3204
3853
|
}
|
|
3205
3854
|
if (await readText(root, edit.path) !== edit.before) throw new Error("Integration input changed during update: " + edit.path);
|
|
3206
|
-
await
|
|
3855
|
+
await fs17.rename(temporary, file);
|
|
3207
3856
|
return true;
|
|
3208
3857
|
} finally {
|
|
3209
|
-
await
|
|
3858
|
+
await fs17.rm(temporary, { force: true });
|
|
3210
3859
|
}
|
|
3211
3860
|
}
|
|
3212
3861
|
var init_files2 = __esm({
|
|
@@ -3227,7 +3876,7 @@ __export(ownership_exports, {
|
|
|
3227
3876
|
ownershipEdit: () => ownershipEdit,
|
|
3228
3877
|
ownershipSchema: () => ownershipSchema
|
|
3229
3878
|
});
|
|
3230
|
-
import { z as
|
|
3879
|
+
import { z as z14 } from "zod";
|
|
3231
3880
|
import { parse } from "smol-toml";
|
|
3232
3881
|
function blockSpan(text2, start, end) {
|
|
3233
3882
|
const starts = text2.split(start).length - 1, ends = text2.split(end).length - 1;
|
|
@@ -3278,30 +3927,30 @@ var init_ownership = __esm({
|
|
|
3278
3927
|
["<!-- mason:agents:start -->", "<!-- mason:agents:end -->"],
|
|
3279
3928
|
["# mason:mcp:start", "# mason:mcp:end"]
|
|
3280
3929
|
];
|
|
3281
|
-
fileSchema =
|
|
3282
|
-
created:
|
|
3283
|
-
initialHash:
|
|
3284
|
-
blocks:
|
|
3285
|
-
mcpHash:
|
|
3286
|
-
hookCommands:
|
|
3287
|
-
hooks:
|
|
3930
|
+
fileSchema = z14.object({
|
|
3931
|
+
created: z14.boolean(),
|
|
3932
|
+
initialHash: z14.string(),
|
|
3933
|
+
blocks: z14.record(z14.string()),
|
|
3934
|
+
mcpHash: z14.string().optional(),
|
|
3935
|
+
hookCommands: z14.array(z14.string()).optional(),
|
|
3936
|
+
hooks: z14.record(z14.array(z14.string())).optional()
|
|
3288
3937
|
});
|
|
3289
|
-
ownershipSchema =
|
|
3938
|
+
ownershipSchema = z14.object({ version: z14.literal(1), files: z14.record(fileSchema) });
|
|
3290
3939
|
}
|
|
3291
3940
|
});
|
|
3292
3941
|
|
|
3293
3942
|
// src/mcp/init.ts
|
|
3294
|
-
import { z as
|
|
3943
|
+
import { z as z15 } from "zod";
|
|
3295
3944
|
var markerSchema, CLAUDE_MD_SECTION, ASSISTANT_SETUP, QUICKSTART_PLAYBOOK, MAP_PLAYBOOK;
|
|
3296
3945
|
var init_init = __esm({
|
|
3297
3946
|
"src/mcp/init.ts"() {
|
|
3298
3947
|
"use strict";
|
|
3299
3948
|
init_storage();
|
|
3300
3949
|
init_files2();
|
|
3301
|
-
markerSchema =
|
|
3302
|
-
version:
|
|
3303
|
-
initializedAt:
|
|
3304
|
-
features:
|
|
3950
|
+
markerSchema = z15.object({
|
|
3951
|
+
version: z15.literal(1),
|
|
3952
|
+
initializedAt: z15.string(),
|
|
3953
|
+
features: z15.object({ confluence: z15.boolean().optional() }).passthrough().optional()
|
|
3305
3954
|
}).passthrough();
|
|
3306
3955
|
CLAUDE_MD_SECTION = `<!-- mason:start -->
|
|
3307
3956
|
## Mason project knowledge
|
|
@@ -3311,12 +3960,14 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
|
|
|
3311
3960
|
- 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.
|
|
3312
3961
|
- Before editing a file \u2192 \`get_impact\` to check references, tests, and historical change partners.
|
|
3313
3962
|
- 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.
|
|
3963
|
+
- 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.
|
|
3314
3964
|
- 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.
|
|
3315
3965
|
- 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.
|
|
3316
3966
|
- 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.
|
|
3317
3967
|
- \`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.
|
|
3318
3968
|
|
|
3319
3969
|
- 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.
|
|
3970
|
+
- 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.
|
|
3320
3971
|
|
|
3321
3972
|
- 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.
|
|
3322
3973
|
|
|
@@ -3332,7 +3983,7 @@ ${CLAUDE_MD_SECTION}`;
|
|
|
3332
3983
|
|
|
3333
3984
|
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.
|
|
3334
3985
|
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.
|
|
3335
|
-
3. When the task reveals a real lesson or constraint,
|
|
3986
|
+
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)\`.
|
|
3336
3987
|
|
|
3337
3988
|
${ASSISTANT_SETUP}
|
|
3338
3989
|
|
|
@@ -3378,8 +4029,8 @@ var git_exports = {};
|
|
|
3378
4029
|
__export(git_exports, {
|
|
3379
4030
|
isGitRepo: () => isGitRepo
|
|
3380
4031
|
});
|
|
3381
|
-
import { execFile as
|
|
3382
|
-
import { promisify as
|
|
4032
|
+
import { execFile as execFile11 } from "child_process";
|
|
4033
|
+
import { promisify as promisify11 } from "util";
|
|
3383
4034
|
async function isGitRepo(dir) {
|
|
3384
4035
|
try {
|
|
3385
4036
|
await exec8("git", ["rev-parse", "--git-dir"], { cwd: dir });
|
|
@@ -3392,7 +4043,7 @@ var exec8;
|
|
|
3392
4043
|
var init_git2 = __esm({
|
|
3393
4044
|
"src/utils/git.ts"() {
|
|
3394
4045
|
"use strict";
|
|
3395
|
-
exec8 =
|
|
4046
|
+
exec8 = promisify11(execFile11);
|
|
3396
4047
|
}
|
|
3397
4048
|
});
|
|
3398
4049
|
|
|
@@ -3408,7 +4059,7 @@ __export(config_exports, {
|
|
|
3408
4059
|
});
|
|
3409
4060
|
import { isDeepStrictEqual } from "util";
|
|
3410
4061
|
import { parse as parse2, stringify } from "smol-toml";
|
|
3411
|
-
import { z as
|
|
4062
|
+
import { z as z16 } from "zod";
|
|
3412
4063
|
async function instructionEdits(root, host) {
|
|
3413
4064
|
const found = await Promise.all(DOC_CANDIDATES.map(async (file) => ({ path: file, text: await readText(root, file) })));
|
|
3414
4065
|
const existing = found.find((f) => f.text !== null);
|
|
@@ -3477,11 +4128,11 @@ async function hookEdits(root, host) {
|
|
|
3477
4128
|
}
|
|
3478
4129
|
async function ancillaryEdits(root) {
|
|
3479
4130
|
const ignore = await readText(root, ".gitignore");
|
|
3480
|
-
const { git:
|
|
4131
|
+
const { git: git4 } = await Promise.resolve().then(() => (init_evidence(), evidence_exports));
|
|
3481
4132
|
const { isGitRepo: isGitRepo2 } = await Promise.resolve().then(() => (init_git2(), git_exports));
|
|
3482
4133
|
let parentIgnored = false;
|
|
3483
4134
|
try {
|
|
3484
|
-
if (await isGitRepo2(root)) parentIgnored = !!(await
|
|
4135
|
+
if (await isGitRepo2(root)) parentIgnored = !!(await git4(root, "check-ignore", "--no-index", ".mason")).trim();
|
|
3485
4136
|
} catch (error) {
|
|
3486
4137
|
if (error.code !== 1) throw error;
|
|
3487
4138
|
}
|
|
@@ -3490,6 +4141,8 @@ async function ancillaryEdits(root) {
|
|
|
3490
4141
|
...retainParentRule ? ["!/.mason/", "/.mason/*"] : [],
|
|
3491
4142
|
"!/.mason/decisions/",
|
|
3492
4143
|
"!/.mason/decisions/**",
|
|
4144
|
+
"!/.mason/reviews/",
|
|
4145
|
+
"!/.mason/reviews/**",
|
|
3493
4146
|
"!/.mason/config.json",
|
|
3494
4147
|
"!/.mason/snapshot.json",
|
|
3495
4148
|
"/.mason/local/",
|
|
@@ -3524,7 +4177,7 @@ var init_config = __esm({
|
|
|
3524
4177
|
init_install();
|
|
3525
4178
|
init_files2();
|
|
3526
4179
|
init_launcher();
|
|
3527
|
-
object =
|
|
4180
|
+
object = z16.record(z16.unknown());
|
|
3528
4181
|
mcpPath = (host) => host === "codex" ? ".codex/config.toml" : ".mcp.json";
|
|
3529
4182
|
TOML_START = "# mason:mcp:start";
|
|
3530
4183
|
TOML_END = "# mason:mcp:end";
|
|
@@ -3542,11 +4195,11 @@ __export(install_exports, {
|
|
|
3542
4195
|
installAutomation: () => installAutomation,
|
|
3543
4196
|
installedAutomation: () => installedAutomation,
|
|
3544
4197
|
planAutomationInstall: () => planAutomationInstall,
|
|
3545
|
-
recordSchema: () =>
|
|
4198
|
+
recordSchema: () => recordSchema2
|
|
3546
4199
|
});
|
|
3547
|
-
import { z as
|
|
4200
|
+
import { z as z17 } from "zod";
|
|
3548
4201
|
async function installationRecord(root) {
|
|
3549
|
-
return
|
|
4202
|
+
return recordSchema2.parse(await readStoreJson(root, AUTOMATION_PATH) ?? { version: 1, hosts: {} });
|
|
3550
4203
|
}
|
|
3551
4204
|
async function installAutomation(dir, host, command2) {
|
|
3552
4205
|
const ws = await workspace(dir);
|
|
@@ -3615,7 +4268,7 @@ async function installedAutomation(dir) {
|
|
|
3615
4268
|
}
|
|
3616
4269
|
return result;
|
|
3617
4270
|
}
|
|
3618
|
-
var groupSchema, automationConfigSchema,
|
|
4271
|
+
var groupSchema, automationConfigSchema, recordSchema2, AUTOMATION_PATH, configPath;
|
|
3619
4272
|
var init_install = __esm({
|
|
3620
4273
|
"src/automation/install.ts"() {
|
|
3621
4274
|
"use strict";
|
|
@@ -3625,9 +4278,9 @@ var init_install = __esm({
|
|
|
3625
4278
|
init_store();
|
|
3626
4279
|
init_files2();
|
|
3627
4280
|
init_ownership();
|
|
3628
|
-
groupSchema =
|
|
3629
|
-
automationConfigSchema =
|
|
3630
|
-
|
|
4281
|
+
groupSchema = z17.object({ hooks: z17.array(z17.object({ type: z17.string(), command: z17.string().optional() }).passthrough()) }).passthrough();
|
|
4282
|
+
automationConfigSchema = z17.object({ hooks: z17.record(z17.array(groupSchema)).optional() }).passthrough();
|
|
4283
|
+
recordSchema2 = z17.object({ version: z17.literal(1), hosts: z17.record(z17.object({ command: z17.string() })) });
|
|
3631
4284
|
AUTOMATION_PATH = ".mason/local/automation.json";
|
|
3632
4285
|
configPath = (host) => host === "claude" ? ".claude/settings.json" : ".codex/hooks.json";
|
|
3633
4286
|
}
|
|
@@ -3641,8 +4294,8 @@ __export(teardown_exports, {
|
|
|
3641
4294
|
});
|
|
3642
4295
|
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
3643
4296
|
import { parse as parse3, stringify as stringify2 } from "smol-toml";
|
|
3644
|
-
import
|
|
3645
|
-
import { z as
|
|
4297
|
+
import fg6 from "fast-glob";
|
|
4298
|
+
import { z as z18 } from "zod";
|
|
3646
4299
|
async function planTeardown(root, selected) {
|
|
3647
4300
|
const edits = [], diagnostics = [];
|
|
3648
4301
|
const inputs = /* @__PURE__ */ new Map();
|
|
@@ -3662,7 +4315,7 @@ async function planTeardown(root, selected) {
|
|
|
3662
4315
|
const setupBefore = await read(SETUP_PATH);
|
|
3663
4316
|
const setup = setupBefore === null ? null : setupSchema.parse(JSON.parse(setupBefore));
|
|
3664
4317
|
const automationBefore = await read(AUTOMATION_PATH);
|
|
3665
|
-
const automation = automationBefore === null ? null :
|
|
4318
|
+
const automation = automationBefore === null ? null : recordSchema2.parse(JSON.parse(automationBefore));
|
|
3666
4319
|
const commands = (host) => new Set([
|
|
3667
4320
|
automation?.hosts[host]?.command,
|
|
3668
4321
|
...ownership.files[configPath(host)]?.hookCommands ?? [],
|
|
@@ -3776,7 +4429,7 @@ async function planTeardown(root, selected) {
|
|
|
3776
4429
|
if (diagnostics.length) edits.splice(instructionEditStart);
|
|
3777
4430
|
if (!diagnostics.length) {
|
|
3778
4431
|
const cleanup = [];
|
|
3779
|
-
const receiptFiles = await
|
|
4432
|
+
const receiptFiles = await fg6(
|
|
3780
4433
|
selected.map((host) => ".mason/reports/automation/*/setup-" + host + ".json"),
|
|
3781
4434
|
{ cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false }
|
|
3782
4435
|
);
|
|
@@ -3789,7 +4442,7 @@ async function planTeardown(root, selected) {
|
|
|
3789
4442
|
if (!remainingHost) await attempt(".mason/local/project.json", async () => {
|
|
3790
4443
|
const before = await read(".mason/local/project.json");
|
|
3791
4444
|
if (before === null) return;
|
|
3792
|
-
|
|
4445
|
+
z18.object({ version: z18.literal(1), initializedAt: z18.string() }).strict().parse(JSON.parse(before));
|
|
3793
4446
|
cleanup.push({ path: ".mason/local/project.json", before, after: null });
|
|
3794
4447
|
});
|
|
3795
4448
|
if (!diagnostics.length) {
|
|
@@ -3861,14 +4514,14 @@ var init_teardown = __esm({
|
|
|
3861
4514
|
init_model();
|
|
3862
4515
|
init_ownership();
|
|
3863
4516
|
hosts = ["codex", "claude"];
|
|
3864
|
-
object2 =
|
|
4517
|
+
object2 = z18.record(z18.unknown());
|
|
3865
4518
|
json = (value) => JSON.stringify(value, null, 2) + "\n";
|
|
3866
4519
|
}
|
|
3867
4520
|
});
|
|
3868
4521
|
|
|
3869
4522
|
// src/review/cochange.ts
|
|
3870
|
-
import { execFile as
|
|
3871
|
-
import { promisify as
|
|
4523
|
+
import { execFile as execFile12 } from "child_process";
|
|
4524
|
+
import { promisify as promisify12 } from "util";
|
|
3872
4525
|
async function buildMatrix(resolvedRoot) {
|
|
3873
4526
|
try {
|
|
3874
4527
|
const { stdout: shallow } = await exec9("git", ["rev-parse", "--is-shallow-repository"], { cwd: resolvedRoot });
|
|
@@ -3933,7 +4586,7 @@ var exec9, HISTORY_COMMITS, MIN_FILE_COMMITS, MIN_SHARED_COMMITS, MIN_COCHANGE_R
|
|
|
3933
4586
|
var init_cochange = __esm({
|
|
3934
4587
|
"src/review/cochange.ts"() {
|
|
3935
4588
|
"use strict";
|
|
3936
|
-
exec9 =
|
|
4589
|
+
exec9 = promisify12(execFile12);
|
|
3937
4590
|
HISTORY_COMMITS = 1500;
|
|
3938
4591
|
MIN_FILE_COMMITS = 5;
|
|
3939
4592
|
MIN_SHARED_COMMITS = 4;
|
|
@@ -3988,7 +4641,7 @@ var init_types2 = __esm({
|
|
|
3988
4641
|
});
|
|
3989
4642
|
|
|
3990
4643
|
// src/review/evidence/vitest.ts
|
|
3991
|
-
import { z as
|
|
4644
|
+
import { z as z19 } from "zod";
|
|
3992
4645
|
function parseVitest(raw, sourceRoot) {
|
|
3993
4646
|
const report = schema.parse(raw);
|
|
3994
4647
|
const findings = [], diagnostics = [];
|
|
@@ -4031,30 +4684,30 @@ function parseVitest(raw, sourceRoot) {
|
|
|
4031
4684
|
diagnostics
|
|
4032
4685
|
};
|
|
4033
4686
|
}
|
|
4034
|
-
var
|
|
4687
|
+
var count3, schema;
|
|
4035
4688
|
var init_vitest = __esm({
|
|
4036
4689
|
"src/review/evidence/vitest.ts"() {
|
|
4037
4690
|
"use strict";
|
|
4038
4691
|
init_paths2();
|
|
4039
4692
|
init_types2();
|
|
4040
|
-
|
|
4041
|
-
schema =
|
|
4042
|
-
success:
|
|
4043
|
-
numTotalTests:
|
|
4044
|
-
numPassedTests:
|
|
4045
|
-
numFailedTests:
|
|
4046
|
-
numPendingTests:
|
|
4047
|
-
numTodoTests:
|
|
4048
|
-
numFailedTestSuites:
|
|
4049
|
-
testResults:
|
|
4050
|
-
name:
|
|
4051
|
-
status:
|
|
4052
|
-
message:
|
|
4053
|
-
assertionResults:
|
|
4054
|
-
fullName:
|
|
4055
|
-
status:
|
|
4056
|
-
failureMessages:
|
|
4057
|
-
location:
|
|
4693
|
+
count3 = z19.number().int().nonnegative();
|
|
4694
|
+
schema = z19.object({
|
|
4695
|
+
success: z19.boolean(),
|
|
4696
|
+
numTotalTests: count3,
|
|
4697
|
+
numPassedTests: count3,
|
|
4698
|
+
numFailedTests: count3,
|
|
4699
|
+
numPendingTests: count3,
|
|
4700
|
+
numTodoTests: count3,
|
|
4701
|
+
numFailedTestSuites: count3,
|
|
4702
|
+
testResults: z19.array(z19.object({
|
|
4703
|
+
name: z19.string().min(1),
|
|
4704
|
+
status: z19.enum(["passed", "failed"]),
|
|
4705
|
+
message: z19.string().optional(),
|
|
4706
|
+
assertionResults: z19.array(z19.object({
|
|
4707
|
+
fullName: z19.string(),
|
|
4708
|
+
status: z19.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
|
|
4709
|
+
failureMessages: z19.array(z19.string()).nullable().optional(),
|
|
4710
|
+
location: z19.object({ line: count3, column: count3 }).nullable().optional()
|
|
4058
4711
|
}))
|
|
4059
4712
|
}))
|
|
4060
4713
|
});
|
|
@@ -4062,7 +4715,7 @@ var init_vitest = __esm({
|
|
|
4062
4715
|
});
|
|
4063
4716
|
|
|
4064
4717
|
// src/review/evidence/sarif.ts
|
|
4065
|
-
import { z as
|
|
4718
|
+
import { z as z20 } from "zod";
|
|
4066
4719
|
function parseSarif(raw, sourceRoot) {
|
|
4067
4720
|
const report = schema2.parse(raw), findings = [], diagnostics = [];
|
|
4068
4721
|
let active = 0, suppressed = 0, absent = 0, unresolved = 0;
|
|
@@ -4153,40 +4806,98 @@ var init_sarif = __esm({
|
|
|
4153
4806
|
"use strict";
|
|
4154
4807
|
init_paths2();
|
|
4155
4808
|
init_types2();
|
|
4156
|
-
index =
|
|
4157
|
-
artifact =
|
|
4158
|
-
message =
|
|
4159
|
-
level =
|
|
4160
|
-
location =
|
|
4161
|
-
schema2 =
|
|
4162
|
-
version:
|
|
4163
|
-
runs:
|
|
4164
|
-
tool:
|
|
4165
|
-
invocations:
|
|
4166
|
-
versionControlProvenance:
|
|
4167
|
-
originalUriBaseIds:
|
|
4168
|
-
artifacts:
|
|
4169
|
-
results:
|
|
4170
|
-
ruleId:
|
|
4809
|
+
index = z20.number().int().nonnegative();
|
|
4810
|
+
artifact = z20.object({ uri: z20.string().optional(), uriBaseId: z20.string().optional(), index: index.optional() });
|
|
4811
|
+
message = z20.object({ text: z20.string().optional(), markdown: z20.string().optional(), id: z20.string().optional(), arguments: z20.array(z20.string()).optional() });
|
|
4812
|
+
level = z20.enum(["error", "warning", "note", "none"]);
|
|
4813
|
+
location = z20.object({ physicalLocation: z20.object({ artifactLocation: artifact.optional(), region: z20.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
|
|
4814
|
+
schema2 = z20.object({
|
|
4815
|
+
version: z20.literal("2.1.0"),
|
|
4816
|
+
runs: z20.array(z20.object({
|
|
4817
|
+
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() }) }),
|
|
4818
|
+
invocations: z20.array(z20.object({ executionSuccessful: z20.boolean(), commandLine: z20.string().optional(), toolExecutionNotifications: z20.array(z20.object({ level: level.optional(), message })).optional() })).optional(),
|
|
4819
|
+
versionControlProvenance: z20.array(z20.object({ revisionId: z20.string().optional() })).optional(),
|
|
4820
|
+
originalUriBaseIds: z20.record(artifact).optional(),
|
|
4821
|
+
artifacts: z20.array(z20.object({ location: artifact.optional() })).optional(),
|
|
4822
|
+
results: z20.array(z20.object({
|
|
4823
|
+
ruleId: z20.string().optional(),
|
|
4171
4824
|
ruleIndex: index.optional(),
|
|
4172
4825
|
message,
|
|
4173
4826
|
level: level.optional(),
|
|
4174
|
-
kind:
|
|
4175
|
-
baselineState:
|
|
4176
|
-
suppressions:
|
|
4177
|
-
locations:
|
|
4178
|
-
relatedLocations:
|
|
4827
|
+
kind: z20.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
|
|
4828
|
+
baselineState: z20.enum(["new", "unchanged", "updated", "absent"]).optional(),
|
|
4829
|
+
suppressions: z20.array(z20.object({ kind: z20.enum(["inSource", "external"]), status: z20.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
|
|
4830
|
+
locations: z20.array(location).optional(),
|
|
4831
|
+
relatedLocations: z20.array(location).optional()
|
|
4179
4832
|
})).optional()
|
|
4180
4833
|
}))
|
|
4181
4834
|
});
|
|
4182
4835
|
}
|
|
4183
4836
|
});
|
|
4184
4837
|
|
|
4838
|
+
// src/review/evidence/check.ts
|
|
4839
|
+
import { z as z21 } from "zod";
|
|
4840
|
+
function parseCheck(raw, sourceRoot) {
|
|
4841
|
+
const report = schema3.parse(raw);
|
|
4842
|
+
const diagnostics = [], findings = [];
|
|
4843
|
+
const counts = { total: report.results.length, passed: 0, failed: 0, skipped: 0 };
|
|
4844
|
+
for (const [index2, result] of report.results.entries()) {
|
|
4845
|
+
counts[result.status]++;
|
|
4846
|
+
const locations = result.locations.flatMap((location2) => {
|
|
4847
|
+
const file = evidencePath(location2.file, sourceRoot);
|
|
4848
|
+
if (!file) {
|
|
4849
|
+
diagnostics.push(`Check location is outside the declared checkout or invalid: ${location2.file}`);
|
|
4850
|
+
return [];
|
|
4851
|
+
}
|
|
4852
|
+
return [{ ...location2, file }];
|
|
4853
|
+
});
|
|
4854
|
+
if (result.status === "failed") findings.push({
|
|
4855
|
+
id: String(index2),
|
|
4856
|
+
...messagePreview([result.name, result.message].filter(Boolean).join("\n")),
|
|
4857
|
+
severity: "error",
|
|
4858
|
+
state: "active",
|
|
4859
|
+
locations
|
|
4860
|
+
});
|
|
4861
|
+
}
|
|
4862
|
+
if (!counts.total) diagnostics.push("No check results were recorded; an empty report is not passing evidence.");
|
|
4863
|
+
if (counts.skipped) diagnostics.push(`${counts.skipped} checks were skipped; verification is incomplete.`);
|
|
4864
|
+
return {
|
|
4865
|
+
outcome: counts.failed ? "failed" : !counts.total ? "unavailable" : !counts.passed ? "skipped" : "passed",
|
|
4866
|
+
counts,
|
|
4867
|
+
findings,
|
|
4868
|
+
incomplete: diagnostics.length > 0,
|
|
4869
|
+
diagnostics,
|
|
4870
|
+
...report.commit ? { reportedCommits: [report.commit] } : {}
|
|
4871
|
+
};
|
|
4872
|
+
}
|
|
4873
|
+
var schema3;
|
|
4874
|
+
var init_check = __esm({
|
|
4875
|
+
"src/review/evidence/check.ts"() {
|
|
4876
|
+
"use strict";
|
|
4877
|
+
init_paths2();
|
|
4878
|
+
init_types2();
|
|
4879
|
+
schema3 = z21.object({
|
|
4880
|
+
version: z21.literal(1),
|
|
4881
|
+
commit: z21.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).optional(),
|
|
4882
|
+
results: z21.array(z21.object({
|
|
4883
|
+
name: z21.string().min(1).max(1e3),
|
|
4884
|
+
status: z21.enum(["passed", "failed", "skipped"]),
|
|
4885
|
+
message: z21.string().max(1e5).optional(),
|
|
4886
|
+
locations: z21.array(z21.object({
|
|
4887
|
+
file: z21.string().min(1).max(4e3),
|
|
4888
|
+
line: z21.number().int().positive().optional(),
|
|
4889
|
+
column: z21.number().int().positive().optional()
|
|
4890
|
+
})).max(100).default([])
|
|
4891
|
+
})).max(1e5)
|
|
4892
|
+
});
|
|
4893
|
+
}
|
|
4894
|
+
});
|
|
4895
|
+
|
|
4185
4896
|
// src/review/evidence.ts
|
|
4186
|
-
import
|
|
4187
|
-
import { z as
|
|
4897
|
+
import path22 from "path";
|
|
4898
|
+
import { z as z22 } from "zod";
|
|
4188
4899
|
function relativeArtifact(root, file) {
|
|
4189
|
-
const relative =
|
|
4900
|
+
const relative = path22.isAbsolute(file) ? path22.relative(root, file) : file;
|
|
4190
4901
|
const normalized = normalizeRepoPath(relative);
|
|
4191
4902
|
if (!normalized) throw new Error(`Evidence artifact must be inside the repository: ${file}`);
|
|
4192
4903
|
return normalized;
|
|
@@ -4225,7 +4936,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
4225
4936
|
let manifest, raw;
|
|
4226
4937
|
try {
|
|
4227
4938
|
manifest = relativeArtifact(root, supplied);
|
|
4228
|
-
raw =
|
|
4939
|
+
raw = z22.object({ version: z22.literal(1), checks: z22.array(z22.unknown()) }).parse(await readStoreJson(root, manifest));
|
|
4229
4940
|
} catch (error) {
|
|
4230
4941
|
output.diagnostics.push(`${supplied}: ${String(error)}`);
|
|
4231
4942
|
continue;
|
|
@@ -4264,7 +4975,8 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
4264
4975
|
counts: {},
|
|
4265
4976
|
incomplete: false,
|
|
4266
4977
|
diagnostics: [],
|
|
4267
|
-
truncated: false
|
|
4978
|
+
truncated: false,
|
|
4979
|
+
...check.environment ? { environment: check.environment } : {}
|
|
4268
4980
|
};
|
|
4269
4981
|
output.checks.push(result);
|
|
4270
4982
|
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.");
|
|
@@ -4277,13 +4989,13 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
4277
4989
|
}
|
|
4278
4990
|
try {
|
|
4279
4991
|
if (!check.report) throw new Error("Completed check has no report artifact.");
|
|
4280
|
-
if (check.kind === "tests" !== (check.report.format === "vitest-json")) throw new Error("Test checks require vitest-json; analysis checks require sarif.");
|
|
4992
|
+
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.");
|
|
4281
4993
|
const file = relativeArtifact(root, check.report.path);
|
|
4282
4994
|
const report = await readStoreJson(root, file);
|
|
4283
4995
|
if (report === null) throw new Error(`Report artifact is missing: ${file}`);
|
|
4284
4996
|
const sourceRoot = check.sourceRoot ?? root;
|
|
4285
|
-
if (!
|
|
4286
|
-
const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : parseSarif(report, sourceRoot);
|
|
4997
|
+
if (!path22.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
|
|
4998
|
+
const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : check.report.format === "mason-check-json" ? parseCheck(report, sourceRoot) : parseSarif(report, sourceRoot);
|
|
4287
4999
|
result.outcome = parsed.outcome;
|
|
4288
5000
|
result.counts = parsed.counts;
|
|
4289
5001
|
result.incomplete = parsed.incomplete;
|
|
@@ -4303,7 +5015,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
4303
5015
|
result.outcome = "failed";
|
|
4304
5016
|
result.diagnostics.push(`Check command exited ${check.exitCode} despite a report with no active failures.`);
|
|
4305
5017
|
}
|
|
4306
|
-
if (check.
|
|
5018
|
+
if (check.kind === "tests" && pairs === void 0) {
|
|
4307
5019
|
try {
|
|
4308
5020
|
pairs = (await buildTestMap(root)).paired;
|
|
4309
5021
|
} catch (error) {
|
|
@@ -4377,29 +5089,36 @@ var init_evidence2 = __esm({
|
|
|
4377
5089
|
init_provenance();
|
|
4378
5090
|
init_vitest();
|
|
4379
5091
|
init_sarif();
|
|
5092
|
+
init_check();
|
|
4380
5093
|
init_types2();
|
|
4381
|
-
checkSchema2 =
|
|
4382
|
-
id:
|
|
4383
|
-
kind:
|
|
4384
|
-
tool:
|
|
4385
|
-
command:
|
|
4386
|
-
commit:
|
|
4387
|
-
workingTreeClean:
|
|
4388
|
-
source:
|
|
4389
|
-
sourceRoot:
|
|
4390
|
-
status:
|
|
4391
|
-
reason:
|
|
4392
|
-
exitCode:
|
|
4393
|
-
|
|
5094
|
+
checkSchema2 = z22.object({
|
|
5095
|
+
id: z22.string().min(1).max(100),
|
|
5096
|
+
kind: z22.enum(["tests", "static-analysis", "security", "complexity", "duplication", "documentation"]),
|
|
5097
|
+
tool: z22.string().min(1).max(200),
|
|
5098
|
+
command: z22.string().min(1).max(2e3),
|
|
5099
|
+
commit: z22.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
|
|
5100
|
+
workingTreeClean: z22.boolean().optional(),
|
|
5101
|
+
source: z22.string().max(2e3).optional(),
|
|
5102
|
+
sourceRoot: z22.string().min(1).max(4e3).optional(),
|
|
5103
|
+
status: z22.enum(["completed", "skipped", "unavailable"]).default("completed"),
|
|
5104
|
+
reason: z22.string().min(1).max(2e3).optional(),
|
|
5105
|
+
exitCode: z22.number().int().nullable().optional(),
|
|
5106
|
+
environment: z22.object({
|
|
5107
|
+
toolVersion: z22.string().min(1).max(200),
|
|
5108
|
+
runtime: z22.string().min(1).max(200).optional(),
|
|
5109
|
+
platform: z22.string().min(1).max(200).optional(),
|
|
5110
|
+
configuration: z22.array(z22.string().min(1).max(4e3)).max(100).optional()
|
|
5111
|
+
}).optional(),
|
|
5112
|
+
report: z22.object({ format: z22.enum(["vitest-json", "sarif", "mason-check-json"]), path: z22.string().min(1).max(4e3) }).optional()
|
|
4394
5113
|
});
|
|
4395
5114
|
}
|
|
4396
5115
|
});
|
|
4397
5116
|
|
|
4398
5117
|
// src/review/review.ts
|
|
4399
|
-
import
|
|
4400
|
-
import
|
|
4401
|
-
import { execFile as
|
|
4402
|
-
import { promisify as
|
|
5118
|
+
import fs18 from "fs/promises";
|
|
5119
|
+
import path23 from "path";
|
|
5120
|
+
import { execFile as execFile13 } from "child_process";
|
|
5121
|
+
import { promisify as promisify13 } from "util";
|
|
4403
5122
|
async function resolveMergeBase(resolvedRoot, base, head) {
|
|
4404
5123
|
try {
|
|
4405
5124
|
const { stdout } = await exec10("git", ["merge-base", base, head], {
|
|
@@ -4426,7 +5145,7 @@ function anchorsTouched(record, changedFiles) {
|
|
|
4426
5145
|
return matchingPaths(record.files, changedFiles);
|
|
4427
5146
|
}
|
|
4428
5147
|
async function computeReview(rootDir, base, options = {}) {
|
|
4429
|
-
const resolvedRoot =
|
|
5148
|
+
const resolvedRoot = path23.resolve(rootDir);
|
|
4430
5149
|
const head = await getCurrentGitHash(resolvedRoot);
|
|
4431
5150
|
if (head === "unknown") return null;
|
|
4432
5151
|
const mergeBase = await resolveMergeBase(resolvedRoot, base, head);
|
|
@@ -4476,7 +5195,7 @@ async function computeReview(rootDir, base, options = {}) {
|
|
|
4476
5195
|
analyzed,
|
|
4477
5196
|
async (relPath) => {
|
|
4478
5197
|
try {
|
|
4479
|
-
await
|
|
5198
|
+
await fs18.access(path23.join(resolvedRoot, relPath));
|
|
4480
5199
|
return true;
|
|
4481
5200
|
} catch {
|
|
4482
5201
|
return false;
|
|
@@ -4521,7 +5240,7 @@ var init_review = __esm({
|
|
|
4521
5240
|
init_cochange();
|
|
4522
5241
|
init_evidence2();
|
|
4523
5242
|
init_snapshot();
|
|
4524
|
-
exec10 =
|
|
5243
|
+
exec10 = promisify13(execFile13);
|
|
4525
5244
|
MAX_ANALYZED_FILES = 50;
|
|
4526
5245
|
}
|
|
4527
5246
|
});
|
|
@@ -4530,7 +5249,7 @@ var init_review = __esm({
|
|
|
4530
5249
|
async function auditSummary(root) {
|
|
4531
5250
|
try {
|
|
4532
5251
|
const report = await computeAudit(root);
|
|
4533
|
-
if (!report) return { status: "no-context-files", reason: "No
|
|
5252
|
+
if (!report) return { status: "no-context-files", reason: "No README.md or agent instruction files found to audit." };
|
|
4534
5253
|
if (!report.gitAvailable) return { status: "unavailable", reason: "Audit needs readable Git history to verify documentation claims.", docs: report.docs };
|
|
4535
5254
|
return {
|
|
4536
5255
|
...report,
|
|
@@ -4839,16 +5558,16 @@ var init_setup = __esm({
|
|
|
4839
5558
|
});
|
|
4840
5559
|
|
|
4841
5560
|
// src/distribution/bundle.ts
|
|
4842
|
-
import
|
|
4843
|
-
import
|
|
5561
|
+
import fs19 from "fs/promises";
|
|
5562
|
+
import path24 from "path";
|
|
4844
5563
|
import { createHash as createHash4 } from "crypto";
|
|
4845
|
-
import { z as
|
|
5564
|
+
import { z as z23 } from "zod";
|
|
4846
5565
|
async function readBundle(root) {
|
|
4847
|
-
const bytes = await
|
|
5566
|
+
const bytes = await fs19.readFile(await storePath(root, "bundle.json"));
|
|
4848
5567
|
const manifest = bundleSchema.parse(JSON.parse(bytes.toString("utf8")));
|
|
4849
5568
|
const windows = manifest.target.startsWith("win32-");
|
|
4850
5569
|
for (const file of Object.keys(manifest.files)) {
|
|
4851
|
-
if (file.includes("\\") || file.split("/").some((p) => !p || p === "." || p === "..") ||
|
|
5570
|
+
if (file.includes("\\") || file.split("/").some((p) => !p || p === "." || p === "..") || path24.isAbsolute(file)) {
|
|
4852
5571
|
throw new Error("Unsafe path in Mason bundle: " + file);
|
|
4853
5572
|
}
|
|
4854
5573
|
}
|
|
@@ -4863,7 +5582,7 @@ async function verifyBundle(root, expectedHash) {
|
|
|
4863
5582
|
if (bundle.manifest.target !== `${process.platform}-${process.arch}`) throw new Error("Mason bundle is for another platform. Install this platform's bundle and rerun setup.");
|
|
4864
5583
|
const files = Object.keys(bundle.manifest.files);
|
|
4865
5584
|
for (let i = 0; i < files.length; i += 8) await Promise.all(files.slice(i, i + 8).map(async (file) => {
|
|
4866
|
-
if (sha256(await
|
|
5585
|
+
if (sha256(await fs19.readFile(await storePath(root, file))) !== bundle.manifest.files[file]) throw new Error("Mason bundle checksum mismatch: " + file);
|
|
4867
5586
|
}));
|
|
4868
5587
|
return bundle;
|
|
4869
5588
|
}
|
|
@@ -4873,31 +5592,31 @@ var init_bundle = __esm({
|
|
|
4873
5592
|
"use strict";
|
|
4874
5593
|
init_storage();
|
|
4875
5594
|
targets = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-arm64", "win32-x64"];
|
|
4876
|
-
digest2 =
|
|
4877
|
-
bundleSchema =
|
|
4878
|
-
format:
|
|
4879
|
-
version:
|
|
4880
|
-
target:
|
|
4881
|
-
nodeVersion:
|
|
4882
|
-
files:
|
|
5595
|
+
digest2 = z23.string().regex(/^[a-f0-9]{64}$/);
|
|
5596
|
+
bundleSchema = z23.object({
|
|
5597
|
+
format: z23.literal(1),
|
|
5598
|
+
version: z23.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
|
|
5599
|
+
target: z23.enum(targets),
|
|
5600
|
+
nodeVersion: z23.string(),
|
|
5601
|
+
files: z23.record(digest2)
|
|
4883
5602
|
}).strict();
|
|
4884
5603
|
sha256 = (bytes) => createHash4("sha256").update(bytes).digest("hex");
|
|
4885
5604
|
}
|
|
4886
5605
|
});
|
|
4887
5606
|
|
|
4888
5607
|
// src/distribution/path.ts
|
|
4889
|
-
import
|
|
5608
|
+
import fs20 from "fs/promises";
|
|
4890
5609
|
import os3 from "os";
|
|
4891
|
-
import
|
|
4892
|
-
import { execFile as
|
|
4893
|
-
import { promisify as
|
|
5610
|
+
import path25 from "path";
|
|
5611
|
+
import { execFile as execFile14 } from "child_process";
|
|
5612
|
+
import { promisify as promisify14 } from "util";
|
|
4894
5613
|
import { createHash as createHash5, randomUUID as randomUUID7 } from "crypto";
|
|
4895
|
-
import { z as
|
|
5614
|
+
import { z as z24 } from "zod";
|
|
4896
5615
|
async function configurePath(bin, previous, save, options = {}) {
|
|
4897
5616
|
const env = options.env ?? process.env, platform = options.platform ?? process.platform;
|
|
4898
5617
|
const changes = [...previous];
|
|
4899
5618
|
const currentPath = platform === "win32" ? Object.entries(env).find(([key]) => key.toLowerCase() === "path")?.[1] : env.PATH;
|
|
4900
|
-
const alreadyOnPath = platform === "win32" ? hasWindowsEntry(currentPath ?? null, bin, env) : (currentPath ?? "").split(":").some((entry) =>
|
|
5619
|
+
const alreadyOnPath = platform === "win32" ? hasWindowsEntry(currentPath ?? null, bin, env) : (currentPath ?? "").split(":").some((entry) => path25.posix.isAbsolute(entry) && path25.posix.resolve(entry) === path25.posix.resolve(bin));
|
|
4901
5620
|
const message2 = alreadyOnPath ? "Run: mason setup --host codex" : "Open a new terminal, then run: mason setup --host codex";
|
|
4902
5621
|
const manual = (reason2) => ({ status: "manual", changes, message: `${reason2}
|
|
4903
5622
|
${alreadyOnPath ? message2 : `Add ${bin} to your PATH, then run: mason setup --host codex`}` });
|
|
@@ -4915,33 +5634,33 @@ ${alreadyOnPath ? message2 : `Add ${bin} to your PATH, then run: mason setup --h
|
|
|
4915
5634
|
}
|
|
4916
5635
|
return { status: "configured", changes, message: message2 };
|
|
4917
5636
|
}
|
|
4918
|
-
const shell =
|
|
5637
|
+
const shell = path25.basename(env.SHELL ?? os3.userInfo().shell ?? "");
|
|
4919
5638
|
const userHome = options.userHome ?? os3.homedir();
|
|
4920
5639
|
let profiles;
|
|
4921
5640
|
if (!["bash", "zsh", "sh"].includes(shell)) return manual(`Automatic PATH setup is unavailable for ${shell || "an unknown shell"}.`);
|
|
4922
|
-
if (env.MASON_PROFILE) profiles = [
|
|
4923
|
-
else if (shell === "zsh") profiles = [
|
|
4924
|
-
else if (shell === "sh") profiles = [
|
|
5641
|
+
if (env.MASON_PROFILE) profiles = [path25.resolve(env.MASON_PROFILE)];
|
|
5642
|
+
else if (shell === "zsh") profiles = [path25.join(env.ZDOTDIR || userHome, ".zshrc")];
|
|
5643
|
+
else if (shell === "sh") profiles = [path25.join(userHome, ".profile")];
|
|
4925
5644
|
else {
|
|
4926
|
-
const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) =>
|
|
5645
|
+
const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) => path25.join(userHome, name));
|
|
4927
5646
|
let login = candidates[2];
|
|
4928
|
-
for (const file of candidates) if (await
|
|
5647
|
+
for (const file of candidates) if (await fs20.lstat(file).then(() => true, (error) => {
|
|
4929
5648
|
if (error.code === "ENOENT") return false;
|
|
4930
5649
|
throw error;
|
|
4931
5650
|
})) {
|
|
4932
5651
|
login = file;
|
|
4933
5652
|
break;
|
|
4934
5653
|
}
|
|
4935
|
-
profiles = [
|
|
5654
|
+
profiles = [path25.join(userHome, ".bashrc"), login];
|
|
4936
5655
|
}
|
|
4937
5656
|
const marker = `# >>> mason PATH ${createHash5("sha256").update(bin).digest("hex").slice(0, 12)} >>>`;
|
|
4938
5657
|
for (const profile of profiles) {
|
|
4939
|
-
const stat = await
|
|
5658
|
+
const stat = await fs20.lstat(profile).catch((error) => {
|
|
4940
5659
|
if (error.code === "ENOENT") return null;
|
|
4941
5660
|
throw error;
|
|
4942
5661
|
});
|
|
4943
|
-
const file = stat?.isSymbolicLink() ? await
|
|
4944
|
-
const original = await
|
|
5662
|
+
const file = stat?.isSymbolicLink() ? await fs20.realpath(profile) : path25.resolve(profile);
|
|
5663
|
+
const original = await fs20.readFile(file).catch((error) => {
|
|
4945
5664
|
if (error.code === "ENOENT" && !stat) return null;
|
|
4946
5665
|
throw error;
|
|
4947
5666
|
});
|
|
@@ -4956,8 +5675,8 @@ ${alreadyOnPath ? message2 : `Add ${bin} to your PATH, then run: mason setup --h
|
|
|
4956
5675
|
changes.push({ kind: "profile", file, block, created: original === null });
|
|
4957
5676
|
await save(changes);
|
|
4958
5677
|
}
|
|
4959
|
-
await
|
|
4960
|
-
const handle = await
|
|
5678
|
+
await fs20.mkdir(path25.dirname(file), { recursive: true });
|
|
5679
|
+
const handle = await fs20.open(file, original === null ? "ax+" : "a+", 384);
|
|
4961
5680
|
try {
|
|
4962
5681
|
if (!(await handle.readFile()).equals(original ?? Buffer.alloc(0))) throw new Error(`Shell profile changed during installation: ${profile}`);
|
|
4963
5682
|
await handle.writeFile(block);
|
|
@@ -4981,13 +5700,13 @@ async function removePath(changes) {
|
|
|
4981
5700
|
await windowsPath("remove", change.entry, current, change.created);
|
|
4982
5701
|
continue;
|
|
4983
5702
|
}
|
|
4984
|
-
const stat = await
|
|
5703
|
+
const stat = await fs20.lstat(change.file).catch((error) => {
|
|
4985
5704
|
if (error.code === "ENOENT") return null;
|
|
4986
5705
|
throw error;
|
|
4987
5706
|
});
|
|
4988
5707
|
if (!stat) continue;
|
|
4989
5708
|
if (!stat.isFile()) throw new Error("profile type changed");
|
|
4990
|
-
const bytes = await
|
|
5709
|
+
const bytes = await fs20.readFile(change.file), text2 = bytes.toString("utf8");
|
|
4991
5710
|
if (!Buffer.from(text2).equals(bytes)) throw new Error("profile encoding changed");
|
|
4992
5711
|
const index2 = text2.indexOf(change.block);
|
|
4993
5712
|
if (index2 < 0 || text2.indexOf(change.block, index2 + change.block.length) >= 0) throw new Error("PATH block was edited or removed");
|
|
@@ -4997,7 +5716,7 @@ async function removePath(changes) {
|
|
|
4997
5716
|
const temporary = change.file + ".mason-" + randomUUID7();
|
|
4998
5717
|
try {
|
|
4999
5718
|
if (stat.nlink > 1) throw new Error("profile has multiple hard links");
|
|
5000
|
-
const handle = await
|
|
5719
|
+
const handle = await fs20.open(temporary, "wx", stat.mode & 511);
|
|
5001
5720
|
try {
|
|
5002
5721
|
await handle.writeFile(next);
|
|
5003
5722
|
await handle.chmod(stat.mode & 511);
|
|
@@ -5006,11 +5725,11 @@ async function removePath(changes) {
|
|
|
5006
5725
|
} finally {
|
|
5007
5726
|
await handle.close();
|
|
5008
5727
|
}
|
|
5009
|
-
const current = await
|
|
5010
|
-
if (!current.isFile() || current.ino !== stat.ino || !(await
|
|
5011
|
-
await
|
|
5728
|
+
const current = await fs20.lstat(change.file);
|
|
5729
|
+
if (!current.isFile() || current.ino !== stat.ino || !(await fs20.readFile(change.file)).equals(bytes)) throw new Error("profile changed during uninstall");
|
|
5730
|
+
await fs20.rename(temporary, change.file);
|
|
5012
5731
|
} finally {
|
|
5013
|
-
await
|
|
5732
|
+
await fs20.rm(temporary, { force: true });
|
|
5014
5733
|
}
|
|
5015
5734
|
} catch (error) {
|
|
5016
5735
|
warnings.push(`Retained PATH setting ${change.kind === "profile" ? change.file : change.entry}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -5044,20 +5763,20 @@ try {
|
|
|
5044
5763
|
@{ value = $next; kind = $kind } | ConvertTo-Json -Compress
|
|
5045
5764
|
} finally { $key.Dispose() }
|
|
5046
5765
|
`;
|
|
5047
|
-
const { stdout } = await
|
|
5766
|
+
const { stdout } = await execute3("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], { windowsHide: true, timeout: 15e3, maxBuffer: 1024 * 1024 });
|
|
5048
5767
|
return JSON.parse(stdout.replace(/^\uFEFF/, "").trim());
|
|
5049
5768
|
}
|
|
5050
|
-
var
|
|
5769
|
+
var execute3, pathChangeSchema, quote, normalizeWindowsEntry, hasWindowsEntry;
|
|
5051
5770
|
var init_path = __esm({
|
|
5052
5771
|
"src/distribution/path.ts"() {
|
|
5053
5772
|
"use strict";
|
|
5054
|
-
|
|
5055
|
-
pathChangeSchema =
|
|
5056
|
-
|
|
5057
|
-
|
|
5773
|
+
execute3 = promisify14(execFile14);
|
|
5774
|
+
pathChangeSchema = z24.discriminatedUnion("kind", [
|
|
5775
|
+
z24.object({ kind: z24.literal("profile"), file: z24.string(), block: z24.string(), created: z24.boolean() }),
|
|
5776
|
+
z24.object({ kind: z24.literal("windows"), entry: z24.string(), created: z24.boolean() })
|
|
5058
5777
|
]);
|
|
5059
5778
|
quote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
|
|
5060
|
-
normalizeWindowsEntry = (value) =>
|
|
5779
|
+
normalizeWindowsEntry = (value) => path25.win32.normalize(value.trim().replace(/^"(.*)"$/, "$1")).replace(/[\\/]+$/, "").toLowerCase();
|
|
5061
5780
|
hasWindowsEntry = (value, entry, env = process.env) => {
|
|
5062
5781
|
const variables = new Map(Object.entries(env).map(([key, value2]) => [key.toLowerCase(), value2]));
|
|
5063
5782
|
const expand = (part) => part.replace(/%([^%]+)%/g, (match, key) => variables.get(key.toLowerCase()) ?? match);
|
|
@@ -5075,41 +5794,41 @@ __export(install_exports2, {
|
|
|
5075
5794
|
uninstallStandalone: () => uninstallStandalone,
|
|
5076
5795
|
upgradeStandalone: () => upgradeStandalone
|
|
5077
5796
|
});
|
|
5078
|
-
import
|
|
5797
|
+
import fs21 from "fs/promises";
|
|
5079
5798
|
import os4 from "os";
|
|
5080
|
-
import
|
|
5799
|
+
import path26 from "path";
|
|
5081
5800
|
import { fileURLToPath } from "url";
|
|
5082
5801
|
import { spawn } from "child_process";
|
|
5083
5802
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
5084
|
-
import { z as
|
|
5803
|
+
import { z as z25 } from "zod";
|
|
5085
5804
|
async function replace(file, content2, mode = 384) {
|
|
5086
5805
|
const temp = file + ".tmp-" + randomUUID8();
|
|
5087
5806
|
try {
|
|
5088
|
-
await
|
|
5089
|
-
await
|
|
5807
|
+
await fs21.writeFile(temp, content2, { mode, flag: "wx" });
|
|
5808
|
+
await fs21.rename(temp, file);
|
|
5090
5809
|
} finally {
|
|
5091
|
-
await
|
|
5810
|
+
await fs21.rm(temp, { force: true });
|
|
5092
5811
|
}
|
|
5093
5812
|
}
|
|
5094
5813
|
async function installStandalone(source) {
|
|
5095
5814
|
const bundle = await verifyBundle(source);
|
|
5096
|
-
const selectedHome =
|
|
5097
|
-
await
|
|
5098
|
-
const home = await
|
|
5099
|
-
const selectedBin =
|
|
5815
|
+
const selectedHome = path26.resolve(defaultHome());
|
|
5816
|
+
await fs21.mkdir(selectedHome, { recursive: true });
|
|
5817
|
+
const home = await fs21.realpath(selectedHome);
|
|
5818
|
+
const selectedBin = path26.resolve(process.env.MASON_BIN_DIR ?? (process.platform === "win32" ? path26.join(home, "bin") : path26.join(os4.homedir(), ".local/bin")));
|
|
5100
5819
|
return withLock(home, ".install-lock", async () => {
|
|
5101
5820
|
const raw = await readStoreJson(home, "install.json");
|
|
5102
|
-
const previous = raw === null ? null :
|
|
5103
|
-
if (!previous) for (const file of await
|
|
5821
|
+
const previous = raw === null ? null : recordSchema3.parse(raw);
|
|
5822
|
+
if (!previous) for (const file of await fs21.readdir(home)) {
|
|
5104
5823
|
if (file === ".install-lock") continue;
|
|
5105
|
-
const directory =
|
|
5106
|
-
const stat = await
|
|
5107
|
-
if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await
|
|
5824
|
+
const directory = path26.join(home, file);
|
|
5825
|
+
const stat = await fs21.lstat(directory);
|
|
5826
|
+
if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs21.readdir(directory)).length) {
|
|
5108
5827
|
throw new Error("Mason installation directory is not empty and has no ownership receipt.");
|
|
5109
5828
|
}
|
|
5110
5829
|
}
|
|
5111
|
-
await
|
|
5112
|
-
const bin = await
|
|
5830
|
+
await fs21.mkdir(selectedBin, { recursive: true });
|
|
5831
|
+
const bin = await fs21.realpath(selectedBin);
|
|
5113
5832
|
if (previous && (previous.home !== home || previous.bin !== bin)) throw new Error("Installation paths differ from the existing Mason receipt.");
|
|
5114
5833
|
const id = bundle.manifestHash.slice(0, 24);
|
|
5115
5834
|
const destination = await storePath(home, "versions/" + id);
|
|
@@ -5120,10 +5839,10 @@ $previousToken = $env:MASON_UNINSTALL_TOKEN
|
|
|
5120
5839
|
$token = [Guid]::NewGuid().ToString()
|
|
5121
5840
|
$env:MASON_UNINSTALL_TOKEN = $token
|
|
5122
5841
|
try {
|
|
5123
|
-
& ${psQuote(
|
|
5842
|
+
& ${psQuote(path26.join(destination, "node.exe"))} ${psQuote(path26.join(destination, "app/dist/mason.js"))} @args
|
|
5124
5843
|
$code = $LASTEXITCODE
|
|
5125
5844
|
if ($code -eq 0 -and $args.Count -eq 1 -and $args[0] -eq 'uninstall') {
|
|
5126
|
-
if ([IO.File]::ReadAllText(${psQuote(
|
|
5845
|
+
if ([IO.File]::ReadAllText(${psQuote(path26.join(home, ".uninstall-request.txt"))}) -cne $token) { throw 'Mason did not authorize installation cleanup.' }
|
|
5127
5846
|
Remove-Item -LiteralPath ${psQuote(home)} -Recurse -Force
|
|
5128
5847
|
Write-Output ${psQuote("Removed standalone installation: " + home)}
|
|
5129
5848
|
}
|
|
@@ -5132,11 +5851,11 @@ try {
|
|
|
5132
5851
|
finally { $env:MASON_UNINSTALL_TOKEN = $previousToken }
|
|
5133
5852
|
`
|
|
5134
5853
|
} : { mason: `#!/bin/sh
|
|
5135
|
-
exec ${shQuote(
|
|
5854
|
+
exec ${shQuote(path26.join(destination, "node"))} ${shQuote(path26.join(destination, "app/dist/mason.js"))} "$@"
|
|
5136
5855
|
` };
|
|
5137
5856
|
for (const [name, content2] of Object.entries(launchers)) {
|
|
5138
5857
|
const file = await storePath(bin, name);
|
|
5139
|
-
const old = await
|
|
5858
|
+
const old = await fs21.readFile(file, "utf8").catch((error) => {
|
|
5140
5859
|
if (error.code === "ENOENT") return null;
|
|
5141
5860
|
throw error;
|
|
5142
5861
|
});
|
|
@@ -5145,7 +5864,7 @@ exec ${shQuote(path23.join(destination, "node"))} ${shQuote(path23.join(destinat
|
|
|
5145
5864
|
const retired = Object.entries({ ...previous?.previousLaunchers, ...previous?.launchers }).filter(([name]) => !(name in launchers));
|
|
5146
5865
|
const checkRetired = async (name, expected) => {
|
|
5147
5866
|
if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
|
|
5148
|
-
const actual = await
|
|
5867
|
+
const actual = await fs21.readFile(await storePath(bin, name), "utf8").catch((error) => {
|
|
5149
5868
|
if (error.code === "ENOENT") return null;
|
|
5150
5869
|
throw error;
|
|
5151
5870
|
});
|
|
@@ -5155,28 +5874,28 @@ exec ${shQuote(path23.join(destination, "node"))} ${shQuote(path23.join(destinat
|
|
|
5155
5874
|
const receipt = { format: 1, home, bin, current: id, version: bundle.manifest.version, launchers, pathChanges: previous?.pathChanges ?? [] };
|
|
5156
5875
|
await writeStoreJson(home, "install.json", { ...receipt, previousLaunchers: previous?.previousLaunchers ?? previous?.launchers });
|
|
5157
5876
|
const stage = await storePath(home, "versions/.install-" + randomUUID8(), true);
|
|
5158
|
-
await
|
|
5877
|
+
await fs21.mkdir(stage);
|
|
5159
5878
|
try {
|
|
5160
5879
|
for (const file of ["bundle.json", ...Object.keys(bundle.manifest.files)]) {
|
|
5161
5880
|
const from = await storePath(source, file), to = await storePath(stage, file, true);
|
|
5162
|
-
await
|
|
5163
|
-
await
|
|
5881
|
+
await fs21.copyFile(from, to);
|
|
5882
|
+
await fs21.chmod(to, (await fs21.stat(from)).mode & 511);
|
|
5164
5883
|
}
|
|
5165
5884
|
await verifyBundle(stage, bundle.manifestHash);
|
|
5166
|
-
const
|
|
5885
|
+
const exists = await fs21.lstat(destination).then(() => true, (error) => {
|
|
5167
5886
|
if (error.code === "ENOENT") return false;
|
|
5168
5887
|
throw error;
|
|
5169
5888
|
});
|
|
5170
|
-
if (
|
|
5171
|
-
else await
|
|
5889
|
+
if (exists) await verifyBundle(destination, bundle.manifestHash);
|
|
5890
|
+
else await fs21.rename(stage, destination);
|
|
5172
5891
|
for (const [name, content2] of Object.entries(launchers)) await replace(await storePath(bin, name, true), content2, 493);
|
|
5173
5892
|
for (const [name, expected] of retired) {
|
|
5174
5893
|
await checkRetired(name, expected);
|
|
5175
|
-
await
|
|
5894
|
+
await fs21.rm(await storePath(bin, name), { force: true });
|
|
5176
5895
|
}
|
|
5177
5896
|
await writeStoreJson(home, "install.json", receipt);
|
|
5178
5897
|
} finally {
|
|
5179
|
-
await
|
|
5898
|
+
await fs21.rm(stage, { recursive: true, force: true });
|
|
5180
5899
|
}
|
|
5181
5900
|
const pathResult = await configurePath(bin, receipt.pathChanges, async (changes) => {
|
|
5182
5901
|
receipt.pathChanges = changes;
|
|
@@ -5186,12 +5905,12 @@ exec ${shQuote(path23.join(destination, "node"))} ${shQuote(path23.join(destinat
|
|
|
5186
5905
|
});
|
|
5187
5906
|
}
|
|
5188
5907
|
async function currentInstallation() {
|
|
5189
|
-
const app =
|
|
5190
|
-
const bundle =
|
|
5908
|
+
const app = path26.dirname(path26.dirname(fileURLToPath(import.meta.url)));
|
|
5909
|
+
const bundle = path26.dirname(app), home = path26.dirname(path26.dirname(bundle));
|
|
5191
5910
|
const raw = await readStoreJson(home, "install.json");
|
|
5192
5911
|
if (!raw) throw new Error("Run this command from a standalone installation. npm users can upgrade with npm.");
|
|
5193
|
-
const record =
|
|
5194
|
-
if (record.home !== await
|
|
5912
|
+
const record = recordSchema3.parse(raw);
|
|
5913
|
+
if (record.home !== await fs21.realpath(home) || path26.dirname(bundle) !== path26.join(home, "versions")) throw new Error("Invalid standalone installation receipt.");
|
|
5195
5914
|
return { home, bundle, record };
|
|
5196
5915
|
}
|
|
5197
5916
|
async function upgradeStandalone(version) {
|
|
@@ -5199,7 +5918,7 @@ async function upgradeStandalone(version) {
|
|
|
5199
5918
|
const { home, bundle, record } = await currentInstallation();
|
|
5200
5919
|
await verifyBundle(bundle);
|
|
5201
5920
|
const windows = process.platform === "win32";
|
|
5202
|
-
const script =
|
|
5921
|
+
const script = path26.join(bundle, windows ? "install.ps1" : "install.sh");
|
|
5203
5922
|
return new Promise((resolve, reject) => {
|
|
5204
5923
|
const child = spawn(
|
|
5205
5924
|
windows ? "powershell.exe" : "sh",
|
|
@@ -5220,7 +5939,7 @@ async function uninstallStandalone() {
|
|
|
5220
5939
|
for (const [name, expected] of Object.entries(record.launchers)) {
|
|
5221
5940
|
if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
|
|
5222
5941
|
const file = await storePath(record.bin, name);
|
|
5223
|
-
const actual = await
|
|
5942
|
+
const actual = await fs21.readFile(file, "utf8").catch((error) => {
|
|
5224
5943
|
if (error.code === "ENOENT") return null;
|
|
5225
5944
|
throw error;
|
|
5226
5945
|
});
|
|
@@ -5228,12 +5947,12 @@ async function uninstallStandalone() {
|
|
|
5228
5947
|
}
|
|
5229
5948
|
for (const warning of await removePath(record.pathChanges ?? [])) console.error(warning);
|
|
5230
5949
|
if (process.platform === "win32") await replace(await storePath(home, ".uninstall-request.txt", true), token);
|
|
5231
|
-
for (const name of Object.keys(record.launchers)) await
|
|
5950
|
+
for (const name of Object.keys(record.launchers)) await fs21.rm(await storePath(record.bin, name), { force: true });
|
|
5232
5951
|
});
|
|
5233
|
-
if (process.platform !== "win32") await
|
|
5952
|
+
if (process.platform !== "win32") await fs21.rm(home, { recursive: true, force: true });
|
|
5234
5953
|
return home;
|
|
5235
5954
|
}
|
|
5236
|
-
var
|
|
5955
|
+
var recordSchema3, shQuote, psQuote, WINDOWS_BATCH_LAUNCHER, launcherNames, defaultHome;
|
|
5237
5956
|
var init_install2 = __esm({
|
|
5238
5957
|
"src/distribution/install.ts"() {
|
|
5239
5958
|
"use strict";
|
|
@@ -5241,21 +5960,21 @@ var init_install2 = __esm({
|
|
|
5241
5960
|
init_storage();
|
|
5242
5961
|
init_store();
|
|
5243
5962
|
init_path();
|
|
5244
|
-
|
|
5245
|
-
format:
|
|
5246
|
-
home:
|
|
5247
|
-
bin:
|
|
5248
|
-
current:
|
|
5249
|
-
version:
|
|
5250
|
-
launchers:
|
|
5251
|
-
previousLaunchers:
|
|
5252
|
-
pathChanges:
|
|
5963
|
+
recordSchema3 = z25.object({
|
|
5964
|
+
format: z25.literal(1),
|
|
5965
|
+
home: z25.string(),
|
|
5966
|
+
bin: z25.string(),
|
|
5967
|
+
current: z25.string().regex(/^[a-f0-9]{24}$/),
|
|
5968
|
+
version: z25.string(),
|
|
5969
|
+
launchers: z25.record(z25.string()),
|
|
5970
|
+
previousLaunchers: z25.record(z25.string()).optional(),
|
|
5971
|
+
pathChanges: z25.array(pathChangeSchema).optional()
|
|
5253
5972
|
});
|
|
5254
5973
|
shQuote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
|
|
5255
5974
|
psQuote = (s) => "'" + s.replace(/'/g, "''") + "'";
|
|
5256
5975
|
WINDOWS_BATCH_LAUNCHER = '@echo off\r\ngoto #_mason_handoff_# 2>NUL || title %COMSPEC% & powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%~dp0mason-launcher.ps1" %*\r\n';
|
|
5257
5976
|
launcherNames = ["mason", "mason.cmd", "mason.ps1", "mason-launcher.ps1"];
|
|
5258
|
-
defaultHome = () => process.env.MASON_HOME ?? (process.platform === "win32" ?
|
|
5977
|
+
defaultHome = () => process.env.MASON_HOME ?? (process.platform === "win32" ? path26.join(process.env.LOCALAPPDATA ?? os4.homedir(), "Mason") : path26.join(os4.homedir(), ".local/share/mason"));
|
|
5259
5978
|
}
|
|
5260
5979
|
});
|
|
5261
5980
|
|
|
@@ -5385,7 +6104,7 @@ var inputArgs = process.argv.slice(2);
|
|
|
5385
6104
|
var managedLaunch = inputArgs[0] === "--setup-host";
|
|
5386
6105
|
var setupHost = managedLaunch ? inputArgs.splice(0, 2)[1] : void 0;
|
|
5387
6106
|
var [command, ...args] = inputArgs;
|
|
5388
|
-
var usage = `Mason ${"0.
|
|
6107
|
+
var usage = `Mason ${"0.17.0"}
|
|
5389
6108
|
|
|
5390
6109
|
Usage: mason <command> [options]
|
|
5391
6110
|
|
|
@@ -5409,9 +6128,9 @@ try {
|
|
|
5409
6128
|
const active = await prepareLaunch2(setupHost, { allowInactive: command === "auto" && isHookCommand(args) });
|
|
5410
6129
|
if (!active) process.exit(0);
|
|
5411
6130
|
}
|
|
5412
|
-
if (command === "internal-integration-version") console.log(JSON.stringify({ protocol: 1, version: "0.
|
|
6131
|
+
if (command === "internal-integration-version") console.log(JSON.stringify({ protocol: 1, version: "0.17.0" }));
|
|
5413
6132
|
else if (!command || ["--help", "-h", "help"].includes(command)) console.log(usage);
|
|
5414
|
-
else if (["--version", "-v"].includes(command)) console.log("0.
|
|
6133
|
+
else if (["--version", "-v"].includes(command)) console.log("0.17.0");
|
|
5415
6134
|
else if (command === "internal-install") {
|
|
5416
6135
|
if (args.length) throw new Error("internal-install takes no arguments.");
|
|
5417
6136
|
const { installStandalone: installStandalone2 } = await Promise.resolve().then(() => (init_install2(), install_exports2));
|