mason-context 0.14.1 → 0.14.2
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 +7 -0
- package/README.md +1 -1
- package/dist/mason-audit.js +214 -69
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-auto.js +239 -115
- package/dist/mason-auto.js.map +1 -1
- package/dist/mason-mcp.js +266 -142
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason.js +262 -136
- package/dist/mason.js.map +1 -1
- package/package.json +1 -1
package/dist/mason-mcp.js
CHANGED
|
@@ -1394,9 +1394,133 @@ var init_deleted_reference = __esm({
|
|
|
1394
1394
|
}
|
|
1395
1395
|
});
|
|
1396
1396
|
|
|
1397
|
-
// src/audit/
|
|
1398
|
-
import
|
|
1397
|
+
// src/audit/inputs.ts
|
|
1398
|
+
import fs6 from "fs/promises";
|
|
1399
|
+
import * as nativeFs from "fs";
|
|
1399
1400
|
import path9 from "path";
|
|
1401
|
+
import { execFile as execFile6 } from "child_process";
|
|
1402
|
+
import { promisify as promisify6 } from "util";
|
|
1403
|
+
import fg2 from "fast-glob";
|
|
1404
|
+
async function auditInputPath(root, file) {
|
|
1405
|
+
if (file === ".") return fs6.realpath(root);
|
|
1406
|
+
try {
|
|
1407
|
+
return await storePath(root, file);
|
|
1408
|
+
} catch (error) {
|
|
1409
|
+
if (error instanceof Error && error.message.startsWith("Symlink")) {
|
|
1410
|
+
throw new Error(`Audit input contains a symbolic link: ${file}. Its evidence could not be verified.`, { cause: error });
|
|
1411
|
+
}
|
|
1412
|
+
throw error;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
async function readAuditInput(root, file) {
|
|
1416
|
+
try {
|
|
1417
|
+
const value = await readBoundedFile(await auditInputPath(root, file), 10 * 1024 * 1024);
|
|
1418
|
+
if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
|
|
1419
|
+
return value;
|
|
1420
|
+
} catch (error) {
|
|
1421
|
+
if (error.code === "ENOENT") return null;
|
|
1422
|
+
throw error;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
async function auditGlob(root, patterns, options) {
|
|
1426
|
+
if (Array.isArray(patterns) && !patterns.length) return [];
|
|
1427
|
+
const directories = /* @__PURE__ */ new Set();
|
|
1428
|
+
const readdir2 = ((directory, opts, callback) => {
|
|
1429
|
+
const relative = path9.relative(root, directory).split(path9.sep).join("/") || ".";
|
|
1430
|
+
directories.add(relative);
|
|
1431
|
+
if (directories.size > MAX_INPUTS) {
|
|
1432
|
+
callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
auditInputPath(root, relative).then(() => {
|
|
1436
|
+
nativeFs.readdir(directory, opts, callback);
|
|
1437
|
+
}, callback);
|
|
1438
|
+
});
|
|
1439
|
+
const lstat2 = ((file, callback) => {
|
|
1440
|
+
const relative = path9.relative(root, file).split(path9.sep).join("/") || ".";
|
|
1441
|
+
auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
|
|
1442
|
+
});
|
|
1443
|
+
const stream = fg2.stream(patterns, {
|
|
1444
|
+
cwd: root,
|
|
1445
|
+
ignore: options.ignore,
|
|
1446
|
+
onlyFiles: false,
|
|
1447
|
+
onlyDirectories: options.onlyDirectories,
|
|
1448
|
+
objectMode: true,
|
|
1449
|
+
followSymbolicLinks: options.followSymbolicLinks ?? true,
|
|
1450
|
+
fs: { readdir: readdir2, lstat: lstat2 },
|
|
1451
|
+
concurrency: 16
|
|
1452
|
+
});
|
|
1453
|
+
const matches = [];
|
|
1454
|
+
try {
|
|
1455
|
+
for await (const value of stream) {
|
|
1456
|
+
const entry = value;
|
|
1457
|
+
const file = entry.path;
|
|
1458
|
+
if (options.select && !options.select(file)) continue;
|
|
1459
|
+
await auditInputPath(root, file);
|
|
1460
|
+
if (!options.onlyDirectories && !entry.dirent.isFile()) continue;
|
|
1461
|
+
matches.push(file);
|
|
1462
|
+
if (matches.length > MAX_INPUTS) {
|
|
1463
|
+
const largest = [...matches.reduce((counts, item) => {
|
|
1464
|
+
const dir = item.split("/")[0];
|
|
1465
|
+
counts.set(dir, (counts.get(dir) ?? 0) + 1);
|
|
1466
|
+
return counts;
|
|
1467
|
+
}, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
1468
|
+
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.`);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
} finally {
|
|
1472
|
+
stream.destroy();
|
|
1473
|
+
}
|
|
1474
|
+
return matches.sort();
|
|
1475
|
+
}
|
|
1476
|
+
async function gitSourcePaths(root) {
|
|
1477
|
+
const { stdout } = await execute(
|
|
1478
|
+
"git",
|
|
1479
|
+
[
|
|
1480
|
+
"ls-files",
|
|
1481
|
+
"-z",
|
|
1482
|
+
"--cached",
|
|
1483
|
+
"--others",
|
|
1484
|
+
"--exclude-standard",
|
|
1485
|
+
"--",
|
|
1486
|
+
...SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`)
|
|
1487
|
+
],
|
|
1488
|
+
{ cwd: root, maxBuffer: 32 * 1024 * 1024, timeout: 1e4 }
|
|
1489
|
+
);
|
|
1490
|
+
return [...new Set(stdout.split("\0").filter(Boolean))].sort();
|
|
1491
|
+
}
|
|
1492
|
+
async function gitIgnoredPaths(root, files) {
|
|
1493
|
+
if (!files.length) return /* @__PURE__ */ new Set();
|
|
1494
|
+
const operation = execute(
|
|
1495
|
+
"git",
|
|
1496
|
+
["check-ignore", "-z", "--stdin"],
|
|
1497
|
+
{ cwd: root, maxBuffer: 4 * 1024 * 1024, timeout: 1e4 }
|
|
1498
|
+
);
|
|
1499
|
+
operation.child.stdin.on("error", () => {
|
|
1500
|
+
});
|
|
1501
|
+
operation.child.stdin.end(files.join("\0") + "\0");
|
|
1502
|
+
try {
|
|
1503
|
+
return new Set((await operation).stdout.split("\0").filter(Boolean));
|
|
1504
|
+
} catch (error) {
|
|
1505
|
+
if (error.code === 1) return /* @__PURE__ */ new Set();
|
|
1506
|
+
throw error;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
var execute, MAX_INPUTS;
|
|
1510
|
+
var init_inputs = __esm({
|
|
1511
|
+
"src/audit/inputs.ts"() {
|
|
1512
|
+
"use strict";
|
|
1513
|
+
init_files();
|
|
1514
|
+
init_storage();
|
|
1515
|
+
execute = promisify6(execFile6);
|
|
1516
|
+
MAX_INPUTS = 1e5;
|
|
1517
|
+
}
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1520
|
+
// src/audit/checks/new-module.ts
|
|
1521
|
+
import fg3 from "fast-glob";
|
|
1522
|
+
import fs7 from "fs/promises";
|
|
1523
|
+
import path10 from "path";
|
|
1400
1524
|
function escapeRegExp(text2) {
|
|
1401
1525
|
return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1402
1526
|
}
|
|
@@ -1407,22 +1531,6 @@ function isMentioned(combinedDocs, name) {
|
|
|
1407
1531
|
);
|
|
1408
1532
|
return re.test(combinedDocs);
|
|
1409
1533
|
}
|
|
1410
|
-
async function listSubdirs(absDir) {
|
|
1411
|
-
const dirs = await fg2("*", {
|
|
1412
|
-
cwd: absDir,
|
|
1413
|
-
onlyDirectories: true,
|
|
1414
|
-
suppressErrors: true
|
|
1415
|
-
});
|
|
1416
|
-
return dirs.filter((d) => !DIR_DENYLIST.has(d)).sort();
|
|
1417
|
-
}
|
|
1418
|
-
async function countSourceFiles(absDir) {
|
|
1419
|
-
const files = await fg2(SOURCE_GLOB, {
|
|
1420
|
-
cwd: absDir,
|
|
1421
|
-
ignore: SOURCE_IGNORE,
|
|
1422
|
-
suppressErrors: true
|
|
1423
|
-
});
|
|
1424
|
-
return files.length;
|
|
1425
|
-
}
|
|
1426
1534
|
async function checkNewModules(ctx) {
|
|
1427
1535
|
const result = emptyResult();
|
|
1428
1536
|
if (ctx.docs.length === 0) return result;
|
|
@@ -1450,21 +1558,48 @@ async function checkNewModules(ctx) {
|
|
|
1450
1558
|
return result;
|
|
1451
1559
|
}
|
|
1452
1560
|
async function moduleCandidates(root, combinedDocs) {
|
|
1561
|
+
const [config, sourcePaths] = await Promise.all([loadProjectConfig(root), gitSourcePaths(root)]);
|
|
1562
|
+
const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
|
|
1563
|
+
const sources = sourcePaths.filter((file) => !file.split("/").some((part) => part.startsWith(".")));
|
|
1564
|
+
const listSubdirs = async (dir) => {
|
|
1565
|
+
const entries = await fs7.readdir(await auditInputPath(root, dir), { withFileTypes: true });
|
|
1566
|
+
const dirs = entries.filter((entry) => (entry.isDirectory() || entry.isSymbolicLink()) && !entry.name.startsWith(".") && !DIR_DENYLIST.has(entry.name)).map((entry) => path10.posix.join(dir, entry.name));
|
|
1567
|
+
const ignored = await gitIgnoredPaths(root, dirs);
|
|
1568
|
+
const visible = new Set(dirs.filter((dir2) => !ignored.has(dir2) || sources.some((file) => file.startsWith(dir2 + "/"))));
|
|
1569
|
+
return (await auditGlob(root, dir === "." ? "*" : `${fg3.escapePath(dir)}/*`, {
|
|
1570
|
+
ignore,
|
|
1571
|
+
onlyDirectories: true,
|
|
1572
|
+
label: "Module directory discovery",
|
|
1573
|
+
select: (file) => visible.has(file)
|
|
1574
|
+
})).map((file) => path10.posix.basename(file));
|
|
1575
|
+
};
|
|
1576
|
+
const countSourceFiles = async (dir) => {
|
|
1577
|
+
const files = sources.filter((file) => file.startsWith(dir + "/"));
|
|
1578
|
+
let count3 = 0;
|
|
1579
|
+
for (let offset = 0; offset < files.length; offset += 256) {
|
|
1580
|
+
count3 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) => fg3.escapePath(file)), {
|
|
1581
|
+
ignore,
|
|
1582
|
+
followSymbolicLinks: false,
|
|
1583
|
+
label: `Source discovery in ${dir}`
|
|
1584
|
+
})).length;
|
|
1585
|
+
if (count3 > 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.`);
|
|
1586
|
+
}
|
|
1587
|
+
return count3;
|
|
1588
|
+
};
|
|
1453
1589
|
const candidates = [];
|
|
1454
|
-
for (const topDir of await listSubdirs(
|
|
1455
|
-
const absTop = path9.join(root, topDir);
|
|
1590
|
+
for (const topDir of await listSubdirs(".")) {
|
|
1456
1591
|
const topMentioned = isMentioned(combinedDocs, topDir);
|
|
1457
1592
|
if (!topMentioned) {
|
|
1458
|
-
const count3 = await countSourceFiles(
|
|
1593
|
+
const count3 = await countSourceFiles(topDir);
|
|
1459
1594
|
if (count3 >= 1) candidates.push({ dir: topDir, sourceFileCount: count3 });
|
|
1460
1595
|
continue;
|
|
1461
1596
|
}
|
|
1462
|
-
const subdirs = await listSubdirs(
|
|
1597
|
+
const subdirs = await listSubdirs(topDir);
|
|
1463
1598
|
const mentioned = subdirs.filter((s) => isMentioned(combinedDocs, s));
|
|
1464
1599
|
if (mentioned.length < ENUMERATION_THRESHOLD) continue;
|
|
1465
1600
|
for (const sub of subdirs) {
|
|
1466
1601
|
if (isMentioned(combinedDocs, sub)) continue;
|
|
1467
|
-
const count3 = await countSourceFiles(
|
|
1602
|
+
const count3 = await countSourceFiles(`${topDir}/${sub}`);
|
|
1468
1603
|
if (count3 >= SECOND_LEVEL_MIN_SOURCE_FILES) {
|
|
1469
1604
|
candidates.push({ dir: `${topDir}/${sub}`, sourceFileCount: count3 });
|
|
1470
1605
|
}
|
|
@@ -1476,7 +1611,8 @@ var DIR_DENYLIST, SECOND_LEVEL_MIN_SOURCE_FILES, ENUMERATION_THRESHOLD;
|
|
|
1476
1611
|
var init_new_module = __esm({
|
|
1477
1612
|
"src/audit/checks/new-module.ts"() {
|
|
1478
1613
|
"use strict";
|
|
1479
|
-
|
|
1614
|
+
init_files();
|
|
1615
|
+
init_inputs();
|
|
1480
1616
|
init_git();
|
|
1481
1617
|
init_checks();
|
|
1482
1618
|
DIR_DENYLIST = /* @__PURE__ */ new Set([
|
|
@@ -1504,19 +1640,10 @@ var init_new_module = __esm({
|
|
|
1504
1640
|
});
|
|
1505
1641
|
|
|
1506
1642
|
// src/audit/checks/stale-count.ts
|
|
1507
|
-
import
|
|
1508
|
-
import path10 from "path";
|
|
1509
|
-
import fg3 from "fast-glob";
|
|
1510
|
-
async function readIfExists(absPath) {
|
|
1511
|
-
try {
|
|
1512
|
-
return await fs6.readFile(absPath, "utf-8");
|
|
1513
|
-
} catch {
|
|
1514
|
-
return null;
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1643
|
+
import path11 from "path";
|
|
1517
1644
|
async function countGradleModules(root) {
|
|
1518
1645
|
for (const name of ["settings.gradle.kts", "settings.gradle"]) {
|
|
1519
|
-
const content2 = await
|
|
1646
|
+
const content2 = await readAuditInput(root, name);
|
|
1520
1647
|
if (content2 === null) continue;
|
|
1521
1648
|
const members = [];
|
|
1522
1649
|
for (const call of content2.matchAll(/include\s*\(([^)]*)\)/g)) {
|
|
@@ -1530,26 +1657,28 @@ async function countGradleModules(root) {
|
|
|
1530
1657
|
return null;
|
|
1531
1658
|
}
|
|
1532
1659
|
async function countNpmWorkspaces(root) {
|
|
1533
|
-
const pkgRaw = await
|
|
1660
|
+
const pkgRaw = await readAuditInput(root, "package.json");
|
|
1534
1661
|
if (pkgRaw !== null) {
|
|
1662
|
+
let globs = [];
|
|
1535
1663
|
try {
|
|
1536
1664
|
const pkg = JSON.parse(pkgRaw);
|
|
1537
|
-
|
|
1538
|
-
if (globs.length > 0) {
|
|
1539
|
-
const matched = await fg3(
|
|
1540
|
-
globs.map((g) => `${g.replace(/\/+$/, "")}/package.json`),
|
|
1541
|
-
{ cwd: root, ignore: ["**/node_modules/**"] }
|
|
1542
|
-
);
|
|
1543
|
-
return {
|
|
1544
|
-
actual: matched.length,
|
|
1545
|
-
countedFrom: "package.json workspaces",
|
|
1546
|
-
members: matched.map((m) => path10.dirname(m)).sort()
|
|
1547
|
-
};
|
|
1548
|
-
}
|
|
1665
|
+
globs = Array.isArray(pkg.workspaces) ? pkg.workspaces : Array.isArray(pkg.workspaces?.packages) ? pkg.workspaces.packages : [];
|
|
1549
1666
|
} catch {
|
|
1550
1667
|
}
|
|
1668
|
+
if (globs.length > 0) {
|
|
1669
|
+
const matched = await auditGlob(
|
|
1670
|
+
root,
|
|
1671
|
+
globs.map((g) => `${g.replace(/\/+$/, "")}/package.json`),
|
|
1672
|
+
{ ignore: ["**/node_modules/**"], label: "npm workspace discovery" }
|
|
1673
|
+
);
|
|
1674
|
+
return {
|
|
1675
|
+
actual: matched.length,
|
|
1676
|
+
countedFrom: "package.json workspaces",
|
|
1677
|
+
members: matched.map((m) => path11.dirname(m)).sort()
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1551
1680
|
}
|
|
1552
|
-
const pnpmRaw = await
|
|
1681
|
+
const pnpmRaw = await readAuditInput(root, "pnpm-workspace.yaml");
|
|
1553
1682
|
if (pnpmRaw !== null) {
|
|
1554
1683
|
const globs = [];
|
|
1555
1684
|
let inPackages = false;
|
|
@@ -1568,21 +1697,22 @@ async function countNpmWorkspaces(root) {
|
|
|
1568
1697
|
}
|
|
1569
1698
|
}
|
|
1570
1699
|
if (globs.length > 0) {
|
|
1571
|
-
const matched = await
|
|
1700
|
+
const matched = await auditGlob(
|
|
1701
|
+
root,
|
|
1572
1702
|
globs.map((g) => `${g.replace(/\/+$/, "")}/package.json`),
|
|
1573
|
-
{
|
|
1703
|
+
{ ignore: ["**/node_modules/**"], label: "pnpm workspace discovery" }
|
|
1574
1704
|
);
|
|
1575
1705
|
return {
|
|
1576
1706
|
actual: matched.length,
|
|
1577
1707
|
countedFrom: "pnpm-workspace.yaml",
|
|
1578
|
-
members: matched.map((m) =>
|
|
1708
|
+
members: matched.map((m) => path11.dirname(m)).sort()
|
|
1579
1709
|
};
|
|
1580
1710
|
}
|
|
1581
1711
|
}
|
|
1582
1712
|
return null;
|
|
1583
1713
|
}
|
|
1584
1714
|
async function countCargoCrates(root) {
|
|
1585
|
-
const content2 = await
|
|
1715
|
+
const content2 = await readAuditInput(root, "Cargo.toml");
|
|
1586
1716
|
if (content2 === null) return null;
|
|
1587
1717
|
const membersBlock = content2.match(/members\s*=\s*\[([\s\S]*?)\]/);
|
|
1588
1718
|
if (!membersBlock) return null;
|
|
@@ -1593,12 +1723,12 @@ async function countCargoCrates(root) {
|
|
|
1593
1723
|
const members = /* @__PURE__ */ new Set();
|
|
1594
1724
|
for (const entry of entries) {
|
|
1595
1725
|
if (/[*?[\]{}]/.test(entry)) {
|
|
1596
|
-
const matched = await
|
|
1597
|
-
|
|
1598
|
-
|
|
1726
|
+
const matched = await auditGlob(root, `${entry.replace(/\/+$/, "")}/Cargo.toml`, {
|
|
1727
|
+
ignore: ["**/target/**"],
|
|
1728
|
+
label: "Cargo workspace discovery"
|
|
1599
1729
|
});
|
|
1600
|
-
for (const m of matched) members.add(
|
|
1601
|
-
} else if (await
|
|
1730
|
+
for (const m of matched) members.add(path11.dirname(m));
|
|
1731
|
+
} else if (await readAuditInput(root, path11.posix.join(entry, "Cargo.toml")) !== null) {
|
|
1602
1732
|
members.add(entry);
|
|
1603
1733
|
}
|
|
1604
1734
|
}
|
|
@@ -1652,18 +1782,18 @@ var MEMBERS_CAP;
|
|
|
1652
1782
|
var init_stale_count = __esm({
|
|
1653
1783
|
"src/audit/checks/stale-count.ts"() {
|
|
1654
1784
|
"use strict";
|
|
1785
|
+
init_inputs();
|
|
1655
1786
|
init_checks();
|
|
1656
1787
|
MEMBERS_CAP = 50;
|
|
1657
1788
|
}
|
|
1658
1789
|
});
|
|
1659
1790
|
|
|
1660
1791
|
// src/audit/checks/dead-command.ts
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
async function scriptsOf(absManifest) {
|
|
1792
|
+
async function scriptsOf(root, manifest) {
|
|
1793
|
+
const raw = await readAuditInput(root, manifest);
|
|
1794
|
+
if (raw === null) return null;
|
|
1665
1795
|
try {
|
|
1666
|
-
const pkg = JSON.parse(
|
|
1796
|
+
const pkg = JSON.parse(raw);
|
|
1667
1797
|
return pkg && typeof pkg.scripts === "object" && pkg.scripts !== null ? Object.keys(pkg.scripts) : [];
|
|
1668
1798
|
} catch {
|
|
1669
1799
|
return null;
|
|
@@ -1675,7 +1805,7 @@ async function checkDeadCommands(ctx) {
|
|
|
1675
1805
|
(doc) => doc.claims.commands.map((claim) => ({ doc, claim }))
|
|
1676
1806
|
);
|
|
1677
1807
|
if (commandClaims.length === 0) return result;
|
|
1678
|
-
const rootScripts = await scriptsOf(
|
|
1808
|
+
const rootScripts = await scriptsOf(ctx.root, "package.json");
|
|
1679
1809
|
if (rootScripts === null) {
|
|
1680
1810
|
result.skipped.push({
|
|
1681
1811
|
check: "dead-command",
|
|
@@ -1692,7 +1822,7 @@ async function checkDeadCommands(ctx) {
|
|
|
1692
1822
|
const manifests = await commandManifests(ctx.root);
|
|
1693
1823
|
manifestsChecked = ["package.json", ...manifests.sort()];
|
|
1694
1824
|
for (const manifest of manifests) {
|
|
1695
|
-
const scripts = await scriptsOf(
|
|
1825
|
+
const scripts = await scriptsOf(ctx.root, manifest);
|
|
1696
1826
|
for (const name of scripts ?? []) workspaceScripts.add(name);
|
|
1697
1827
|
}
|
|
1698
1828
|
return workspaceScripts;
|
|
@@ -1718,23 +1848,24 @@ async function checkDeadCommands(ctx) {
|
|
|
1718
1848
|
return result;
|
|
1719
1849
|
}
|
|
1720
1850
|
function commandManifests(root) {
|
|
1721
|
-
return
|
|
1722
|
-
|
|
1723
|
-
|
|
1851
|
+
return auditGlob(root, "**/package.json", {
|
|
1852
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", ".mason/reports/**", "package.json"],
|
|
1853
|
+
label: "Command manifest discovery"
|
|
1724
1854
|
});
|
|
1725
1855
|
}
|
|
1726
1856
|
var AVAILABLE_SCRIPTS_CAP;
|
|
1727
1857
|
var init_dead_command = __esm({
|
|
1728
1858
|
"src/audit/checks/dead-command.ts"() {
|
|
1729
1859
|
"use strict";
|
|
1860
|
+
init_inputs();
|
|
1730
1861
|
init_checks();
|
|
1731
1862
|
AVAILABLE_SCRIPTS_CAP = 30;
|
|
1732
1863
|
}
|
|
1733
1864
|
});
|
|
1734
1865
|
|
|
1735
1866
|
// src/audit/release-metadata.ts
|
|
1736
|
-
import { execFile as
|
|
1737
|
-
import { promisify as
|
|
1867
|
+
import { execFile as execFile7 } from "child_process";
|
|
1868
|
+
import { promisify as promisify7 } from "util";
|
|
1738
1869
|
async function git(root, args) {
|
|
1739
1870
|
return (await exec6("git", args, { cwd: root, timeout: 1e4, maxBuffer: 2 * 1024 * 1024 })).stdout;
|
|
1740
1871
|
}
|
|
@@ -1786,7 +1917,7 @@ var exec6;
|
|
|
1786
1917
|
var init_release_metadata = __esm({
|
|
1787
1918
|
"src/audit/release-metadata.ts"() {
|
|
1788
1919
|
"use strict";
|
|
1789
|
-
exec6 =
|
|
1920
|
+
exec6 = promisify7(execFile7);
|
|
1790
1921
|
}
|
|
1791
1922
|
});
|
|
1792
1923
|
|
|
@@ -2738,8 +2869,8 @@ If a build is interrupted, its partials remain in \`.mason/partial-snapshots/\`.
|
|
|
2738
2869
|
});
|
|
2739
2870
|
|
|
2740
2871
|
// src/review/cochange.ts
|
|
2741
|
-
import { execFile as
|
|
2742
|
-
import { promisify as
|
|
2872
|
+
import { execFile as execFile12 } from "child_process";
|
|
2873
|
+
import { promisify as promisify12 } from "util";
|
|
2743
2874
|
async function buildMatrix(resolvedRoot) {
|
|
2744
2875
|
try {
|
|
2745
2876
|
const { stdout: shallow } = await exec11("git", ["rev-parse", "--is-shallow-repository"], { cwd: resolvedRoot });
|
|
@@ -2804,7 +2935,7 @@ var exec11, HISTORY_COMMITS, MIN_FILE_COMMITS, MIN_SHARED_COMMITS, MIN_COCHANGE_
|
|
|
2804
2935
|
var init_cochange = __esm({
|
|
2805
2936
|
"src/review/cochange.ts"() {
|
|
2806
2937
|
"use strict";
|
|
2807
|
-
exec11 =
|
|
2938
|
+
exec11 = promisify12(execFile12);
|
|
2808
2939
|
HISTORY_COMMITS = 1500;
|
|
2809
2940
|
MIN_FILE_COMMITS = 5;
|
|
2810
2941
|
MIN_SHARED_COMMITS = 4;
|
|
@@ -3269,8 +3400,8 @@ var init_evidence = __esm({
|
|
|
3269
3400
|
// src/review/review.ts
|
|
3270
3401
|
import fs13 from "fs/promises";
|
|
3271
3402
|
import path20 from "path";
|
|
3272
|
-
import { execFile as
|
|
3273
|
-
import { promisify as
|
|
3403
|
+
import { execFile as execFile13 } from "child_process";
|
|
3404
|
+
import { promisify as promisify13 } from "util";
|
|
3274
3405
|
async function resolveMergeBase(resolvedRoot, base, head) {
|
|
3275
3406
|
try {
|
|
3276
3407
|
const { stdout } = await exec12("git", ["merge-base", base, head], {
|
|
@@ -3392,7 +3523,7 @@ var init_review = __esm({
|
|
|
3392
3523
|
init_cochange();
|
|
3393
3524
|
init_evidence();
|
|
3394
3525
|
init_snapshot();
|
|
3395
|
-
exec12 =
|
|
3526
|
+
exec12 = promisify13(execFile13);
|
|
3396
3527
|
MAX_ANALYZED_FILES = 50;
|
|
3397
3528
|
}
|
|
3398
3529
|
});
|
|
@@ -3487,8 +3618,8 @@ __export(impact_exports, {
|
|
|
3487
3618
|
analyzeImpact: () => analyzeImpact
|
|
3488
3619
|
});
|
|
3489
3620
|
import path21 from "path";
|
|
3490
|
-
import { execFile as
|
|
3491
|
-
import { promisify as
|
|
3621
|
+
import { execFile as execFile14 } from "child_process";
|
|
3622
|
+
import { promisify as promisify14 } from "util";
|
|
3492
3623
|
async function analyzeImpact(rootDir, targetFiles) {
|
|
3493
3624
|
const resolvedRoot = path21.resolve(rootDir);
|
|
3494
3625
|
const resolvedTargets = await resolveTargetFiles(resolvedRoot, targetFiles);
|
|
@@ -3655,7 +3786,7 @@ var init_impact = __esm({
|
|
|
3655
3786
|
"use strict";
|
|
3656
3787
|
init_files();
|
|
3657
3788
|
init_paths();
|
|
3658
|
-
exec13 =
|
|
3789
|
+
exec13 = promisify14(execFile14);
|
|
3659
3790
|
}
|
|
3660
3791
|
});
|
|
3661
3792
|
|
|
@@ -4027,9 +4158,9 @@ __export(evidence_exports, {
|
|
|
4027
4158
|
import fs15 from "fs/promises";
|
|
4028
4159
|
import path23 from "path";
|
|
4029
4160
|
import { createHash as createHash4 } from "crypto";
|
|
4030
|
-
import { execFile as
|
|
4031
|
-
import { promisify as
|
|
4032
|
-
import
|
|
4161
|
+
import { execFile as execFile15 } from "child_process";
|
|
4162
|
+
import { promisify as promisify15 } from "util";
|
|
4163
|
+
import fg5 from "fast-glob";
|
|
4033
4164
|
import { z as z11 } from "zod";
|
|
4034
4165
|
async function git2(root, ...args) {
|
|
4035
4166
|
return (await exec14("git", args, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout;
|
|
@@ -4045,34 +4176,13 @@ async function workspace(dir) {
|
|
|
4045
4176
|
}
|
|
4046
4177
|
return { root, gitDir, branch, directory: ".mason/reports/automation/" + hash([root, gitDir, branch]).slice(0, 24) };
|
|
4047
4178
|
}
|
|
4048
|
-
async function content(root, file) {
|
|
4049
|
-
try {
|
|
4050
|
-
const value = await readBoundedFile(await storePath(root, file), 10 * 1024 * 1024);
|
|
4051
|
-
if (value === null) throw new Error("Unreadable or oversized audit input: " + file);
|
|
4052
|
-
return value;
|
|
4053
|
-
} catch (error) {
|
|
4054
|
-
if (error.code === "ENOENT") return null;
|
|
4055
|
-
throw error;
|
|
4056
|
-
}
|
|
4057
|
-
}
|
|
4058
4179
|
async function readInputs(root) {
|
|
4059
|
-
const [headText, docStatus,
|
|
4180
|
+
const [headText, docStatus, shallowPath, replacements] = await Promise.all([
|
|
4060
4181
|
git2(root, "rev-parse", "HEAD"),
|
|
4061
4182
|
git2(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...DOC_CANDIDATES),
|
|
4062
|
-
fg6("**/*", {
|
|
4063
|
-
cwd: root,
|
|
4064
|
-
dot: true,
|
|
4065
|
-
onlyFiles: false,
|
|
4066
|
-
followSymbolicLinks: false,
|
|
4067
|
-
objectMode: true,
|
|
4068
|
-
ignore: SOURCE_IGNORE
|
|
4069
|
-
}),
|
|
4070
4183
|
git2(root, "rev-parse", "--git-path", "shallow"),
|
|
4071
4184
|
git2(root, "for-each-ref", "--format=%(refname) %(objectname)", "refs/replace")
|
|
4072
4185
|
]);
|
|
4073
|
-
const entries = inventory.filter((f) => !internal(f.path) && f.path !== ".git").sort((a, b) => a.path.localeCompare(b.path));
|
|
4074
|
-
const files = entries.map((f) => f.path);
|
|
4075
|
-
if (files.length > 1e5) throw new Error("Automation input inventory exceeds 100,000 paths; use an explicit scoped audit.");
|
|
4076
4186
|
const head = headText.trim();
|
|
4077
4187
|
let shallow = null;
|
|
4078
4188
|
try {
|
|
@@ -4093,9 +4203,19 @@ async function readInputs(root) {
|
|
|
4093
4203
|
claims.push([claim.path, await exists2(claim.path), await exists2(path23.dirname(claim.path))]);
|
|
4094
4204
|
}
|
|
4095
4205
|
}
|
|
4096
|
-
if (entries.some((f) => f.dirent.isSymbolicLink())) throw new Error("Automation inventory contains a symbolic link; use an explicit audit to inspect its scope. No cached verification was recorded.");
|
|
4097
4206
|
const combinedDocs = docContents.map(([, text2]) => text2 ?? "").join("\n");
|
|
4098
4207
|
const countClaims = docContents.flatMap(([, text2]) => text2 ? extractClaims(text2).counts : []);
|
|
4208
|
+
const commandClaims = docContents.flatMap(([, text2]) => text2 ? extractClaims(text2).commands : []);
|
|
4209
|
+
const rootPackage = await content(root, "package.json");
|
|
4210
|
+
let rootScripts = null;
|
|
4211
|
+
if (rootPackage !== null) {
|
|
4212
|
+
try {
|
|
4213
|
+
const pkg = JSON.parse(rootPackage);
|
|
4214
|
+
rootScripts = pkg?.scripts && typeof pkg.scripts === "object" ? Object.keys(pkg.scripts) : [];
|
|
4215
|
+
} catch {
|
|
4216
|
+
}
|
|
4217
|
+
}
|
|
4218
|
+
const needsWorkspaceScripts = rootScripts !== null && commandClaims.some((claim) => !rootScripts.includes(claim.scriptName));
|
|
4099
4219
|
const decisionDirectory = await storePath(root, ".mason/decisions");
|
|
4100
4220
|
const decisionPresence = await fs15.lstat(decisionDirectory).then((stat) => stat.isDirectory() ? "directory" : "file", (error) => {
|
|
4101
4221
|
if (error.code === "ENOENT") return "absent";
|
|
@@ -4104,16 +4224,19 @@ async function readInputs(root) {
|
|
|
4104
4224
|
const [modules, counts, manifests, decisionFiles] = await Promise.all([
|
|
4105
4225
|
combinedDocs ? moduleCandidates(root, combinedDocs) : [],
|
|
4106
4226
|
Promise.all(countClaims.map((claim) => resolveCountSource(root, claim))),
|
|
4107
|
-
commandManifests(root),
|
|
4108
|
-
|
|
4227
|
+
needsWorkspaceScripts ? commandManifests(root) : [],
|
|
4228
|
+
fg5(".mason/decisions/*.json", { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false })
|
|
4109
4229
|
]);
|
|
4110
|
-
const packages =
|
|
4111
|
-
const decisions = await Promise.all(decisionFiles.sort().map(async (file) =>
|
|
4230
|
+
const packages = [["package.json", rootPackage], ...await Promise.all(manifests.sort().map(async (file) => [file, await content(root, file)]))];
|
|
4231
|
+
const decisions = await Promise.all(decisionFiles.sort().map(async (file) => {
|
|
4232
|
+
await storePath(root, file);
|
|
4233
|
+
return [file, await content(root, file)];
|
|
4234
|
+
}));
|
|
4112
4235
|
const [status, index2] = decisions.length ? await Promise.all([
|
|
4113
4236
|
git2(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".", ":(exclude).mason/reports"),
|
|
4114
4237
|
git2(root, "ls-files", "--stage", "-z", "--", ".", ":(exclude).mason/reports")
|
|
4115
4238
|
]) : ["", ""];
|
|
4116
|
-
const common = [
|
|
4239
|
+
const common = [3, engineVersion, head, shallow, replacements, docContents];
|
|
4117
4240
|
const keys = {
|
|
4118
4241
|
"deleted-reference": hash([common, claims, docStatus]),
|
|
4119
4242
|
"new-module": hash([common, modules]),
|
|
@@ -4149,7 +4272,7 @@ function checkCache(raw, inputs) {
|
|
|
4149
4272
|
return { version: 1, entries: canonical, digest: hash(canonical) };
|
|
4150
4273
|
} };
|
|
4151
4274
|
}
|
|
4152
|
-
var exec14, engineVersion, hash, internal, cacheSchema;
|
|
4275
|
+
var exec14, engineVersion, hash, content, internal, cacheSchema;
|
|
4153
4276
|
var init_evidence2 = __esm({
|
|
4154
4277
|
"src/automation/evidence.ts"() {
|
|
4155
4278
|
"use strict";
|
|
@@ -4157,14 +4280,15 @@ var init_evidence2 = __esm({
|
|
|
4157
4280
|
init_claims();
|
|
4158
4281
|
init_repair();
|
|
4159
4282
|
init_checks();
|
|
4160
|
-
|
|
4283
|
+
init_inputs();
|
|
4161
4284
|
init_new_module();
|
|
4162
4285
|
init_stale_count();
|
|
4163
4286
|
init_dead_command();
|
|
4164
4287
|
init_storage();
|
|
4165
|
-
exec14 =
|
|
4166
|
-
engineVersion = true ? "0.14.
|
|
4288
|
+
exec14 = promisify15(execFile15);
|
|
4289
|
+
engineVersion = true ? "0.14.2" : "development";
|
|
4167
4290
|
hash = (value) => createHash4("sha256").update(JSON.stringify(value)).digest("hex");
|
|
4291
|
+
content = readAuditInput;
|
|
4168
4292
|
internal = (file) => file === ".mason" || file === ".mason/reports" || file.startsWith(".mason/reports/");
|
|
4169
4293
|
cacheSchema = z11.object({ version: z11.literal(1), entries: z11.record(z11.object({ key: z11.string(), result: checkResultSchema })), digest: z11.string() });
|
|
4170
4294
|
}
|
|
@@ -4709,8 +4833,8 @@ import fs18 from "fs/promises";
|
|
|
4709
4833
|
import path25 from "path";
|
|
4710
4834
|
import { fileURLToPath } from "url";
|
|
4711
4835
|
import { randomUUID as randomUUID5, createHash as createHash6 } from "crypto";
|
|
4712
|
-
import { execFile as
|
|
4713
|
-
import { promisify as
|
|
4836
|
+
import { execFile as execFile16 } from "child_process";
|
|
4837
|
+
import { promisify as promisify16 } from "util";
|
|
4714
4838
|
async function packageRoot() {
|
|
4715
4839
|
let directory = path25.dirname(fileURLToPath(import.meta.url));
|
|
4716
4840
|
for (let i = 0; i < 8; i++) {
|
|
@@ -4818,7 +4942,7 @@ var init_runtime2 = __esm({
|
|
|
4818
4942
|
init_storage();
|
|
4819
4943
|
init_evidence2();
|
|
4820
4944
|
init_bundle();
|
|
4821
|
-
exec15 =
|
|
4945
|
+
exec15 = promisify16(execFile16);
|
|
4822
4946
|
checksum = (bytes) => createHash6("sha256").update(bytes).digest("hex");
|
|
4823
4947
|
BINARIES = ["dist/mason-auto.js", "dist/mason-mcp.js"];
|
|
4824
4948
|
}
|
|
@@ -5533,8 +5657,8 @@ __export(config_exports, {
|
|
|
5533
5657
|
import fs21 from "fs/promises";
|
|
5534
5658
|
import path27 from "path";
|
|
5535
5659
|
import os3 from "os";
|
|
5536
|
-
import { execFile as
|
|
5537
|
-
import { promisify as
|
|
5660
|
+
import { execFile as execFile17 } from "child_process";
|
|
5661
|
+
import { promisify as promisify17 } from "util";
|
|
5538
5662
|
function configDir() {
|
|
5539
5663
|
return path27.join(os3.homedir(), ".mason");
|
|
5540
5664
|
}
|
|
@@ -5590,7 +5714,7 @@ var exec16, DEFAULT_MODELS;
|
|
|
5590
5714
|
var init_config2 = __esm({
|
|
5591
5715
|
"src/llm/config.ts"() {
|
|
5592
5716
|
"use strict";
|
|
5593
|
-
exec16 =
|
|
5717
|
+
exec16 = promisify17(execFile17);
|
|
5594
5718
|
DEFAULT_MODELS = {
|
|
5595
5719
|
claude: "claude-sonnet-4-20250514",
|
|
5596
5720
|
gemini: "gemini-2.5-flash",
|
|
@@ -5789,8 +5913,8 @@ var init_diff = __esm({
|
|
|
5789
5913
|
});
|
|
5790
5914
|
|
|
5791
5915
|
// src/llm/providers.ts
|
|
5792
|
-
import { execFile as
|
|
5793
|
-
import { promisify as
|
|
5916
|
+
import { execFile as execFile18, spawn } from "child_process";
|
|
5917
|
+
import { promisify as promisify18 } from "util";
|
|
5794
5918
|
async function callLLM(config, userMessage, systemPrompt) {
|
|
5795
5919
|
const model = config.model ?? getDefaultModel(config.provider);
|
|
5796
5920
|
const system = systemPrompt ?? CLAUDE_MD_SYSTEM_PROMPT;
|
|
@@ -5952,7 +6076,7 @@ var init_providers = __esm({
|
|
|
5952
6076
|
"src/llm/providers.ts"() {
|
|
5953
6077
|
"use strict";
|
|
5954
6078
|
init_config2();
|
|
5955
|
-
exec17 =
|
|
6079
|
+
exec17 = promisify18(execFile18);
|
|
5956
6080
|
CLAUDE_MD_SYSTEM_PROMPT = `You are Mason, a context engineering tool. You've been given a comprehensive analysis of a codebase including:
|
|
5957
6081
|
- Git history stats (commit patterns, frequently changed files, stale directories)
|
|
5958
6082
|
- Project structure (directory layout, file counts by type)
|
|
@@ -6291,8 +6415,8 @@ import { z as z19 } from "zod";
|
|
|
6291
6415
|
init_repair();
|
|
6292
6416
|
import fs23 from "fs/promises";
|
|
6293
6417
|
import path29 from "path";
|
|
6294
|
-
import { execFile as
|
|
6295
|
-
import { promisify as
|
|
6418
|
+
import { execFile as execFile19 } from "child_process";
|
|
6419
|
+
import { promisify as promisify19 } from "util";
|
|
6296
6420
|
|
|
6297
6421
|
// src/audit/cli.ts
|
|
6298
6422
|
init_audit();
|
|
@@ -6386,15 +6510,15 @@ function formatFixPrompt(report, baselinePath) {
|
|
|
6386
6510
|
}
|
|
6387
6511
|
|
|
6388
6512
|
// src/analyzers/git-history.ts
|
|
6389
|
-
import { execFile as
|
|
6390
|
-
import { promisify as
|
|
6513
|
+
import { execFile as execFile8 } from "child_process";
|
|
6514
|
+
import { promisify as promisify8 } from "util";
|
|
6391
6515
|
|
|
6392
6516
|
// src/analyzers/base.ts
|
|
6393
6517
|
import fs11 from "fs/promises";
|
|
6394
|
-
import
|
|
6518
|
+
import fg4 from "fast-glob";
|
|
6395
6519
|
var BaseAnalyzer = class {
|
|
6396
6520
|
async findFiles(patterns, root) {
|
|
6397
|
-
return
|
|
6521
|
+
return fg4(patterns, {
|
|
6398
6522
|
cwd: root,
|
|
6399
6523
|
ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"],
|
|
6400
6524
|
absolute: true
|
|
@@ -6424,7 +6548,7 @@ var BaseAnalyzer = class {
|
|
|
6424
6548
|
};
|
|
6425
6549
|
|
|
6426
6550
|
// src/analyzers/git-history.ts
|
|
6427
|
-
var exec7 =
|
|
6551
|
+
var exec7 = promisify8(execFile8);
|
|
6428
6552
|
var GitHistoryAnalyzer = class extends BaseAnalyzer {
|
|
6429
6553
|
name = "git-history";
|
|
6430
6554
|
async analyze(context) {
|
|
@@ -6594,9 +6718,9 @@ async function runAll(context) {
|
|
|
6594
6718
|
}
|
|
6595
6719
|
|
|
6596
6720
|
// src/utils/git.ts
|
|
6597
|
-
import { execFile as
|
|
6598
|
-
import { promisify as
|
|
6599
|
-
var exec8 =
|
|
6721
|
+
import { execFile as execFile9 } from "child_process";
|
|
6722
|
+
import { promisify as promisify9 } from "util";
|
|
6723
|
+
var exec8 = promisify9(execFile9);
|
|
6600
6724
|
async function isGitRepo(dir) {
|
|
6601
6725
|
try {
|
|
6602
6726
|
await exec8("git", ["rev-parse", "--git-dir"], { cwd: dir });
|
|
@@ -6609,9 +6733,9 @@ async function isGitRepo(dir) {
|
|
|
6609
6733
|
// src/mcp/sampler.ts
|
|
6610
6734
|
init_files();
|
|
6611
6735
|
import path18 from "path";
|
|
6612
|
-
import { execFile as
|
|
6613
|
-
import { promisify as
|
|
6614
|
-
var exec9 =
|
|
6736
|
+
import { execFile as execFile10 } from "child_process";
|
|
6737
|
+
import { promisify as promisify10 } from "util";
|
|
6738
|
+
var exec9 = promisify10(execFile10);
|
|
6615
6739
|
var CONFIG_FILES = [
|
|
6616
6740
|
// Build & project config
|
|
6617
6741
|
"package.json",
|
|
@@ -6903,10 +7027,10 @@ init_drift();
|
|
|
6903
7027
|
init_paths();
|
|
6904
7028
|
init_files();
|
|
6905
7029
|
import { createHash as createHash3 } from "crypto";
|
|
6906
|
-
import { execFile as
|
|
6907
|
-
import { promisify as
|
|
7030
|
+
import { execFile as execFile11 } from "child_process";
|
|
7031
|
+
import { promisify as promisify11 } from "util";
|
|
6908
7032
|
import { z as z4 } from "zod";
|
|
6909
|
-
var exec10 =
|
|
7033
|
+
var exec10 = promisify11(execFile11);
|
|
6910
7034
|
var requestSchema = z4.object({
|
|
6911
7035
|
id: z4.string().regex(/^[a-zA-Z0-9_-]+$/),
|
|
6912
7036
|
action: z4.enum(["prepare", "accept", "reaffirm", "retire"]).default("prepare"),
|
|
@@ -7187,7 +7311,7 @@ function batchIdFor(offset) {
|
|
|
7187
7311
|
// src/mcp/tools.ts
|
|
7188
7312
|
init_init();
|
|
7189
7313
|
init_onboarding();
|
|
7190
|
-
var exec18 =
|
|
7314
|
+
var exec18 = promisify19(execFile19);
|
|
7191
7315
|
async function buildContext(dir) {
|
|
7192
7316
|
return {
|
|
7193
7317
|
rootDir: dir,
|
|
@@ -8113,7 +8237,7 @@ function createMcpServer() {
|
|
|
8113
8237
|
const server = new McpServer(
|
|
8114
8238
|
{
|
|
8115
8239
|
name: "mason",
|
|
8116
|
-
version: "0.14.
|
|
8240
|
+
version: "0.14.2"
|
|
8117
8241
|
},
|
|
8118
8242
|
{
|
|
8119
8243
|
instructions: 'Mason retrieves recorded decisions, file impact, and optional feature/flow maps. Use get_context with the task and known files, and get_impact before editing. Save learned rationale and constraints with save_decision; review and commit the local records through the project workflow. These tools work without initialization or a concept map. Consult trust and diagnostics: changed or unknown freshness needs source inspection, failed verification needs correction, and proposals are suggestions and legacy records are unreviewed. Accepted decisions are recorded team constraints, still subject to freshness checks. Use review_decision to prepare code evidence and record only authorized acceptance, reaffirmation, or retirement. Never invent a reviewer or treat these recorded identities as authenticated approval. mason_init returns documentation audit and committed-diff review findings with a quickstart guide. Use mode: "map" only when a full architecture map is requested. A missing map is not a setup failure; use decisions and source evidence. get_snapshot provides architecture navigation when a map is available. The mason-audit and mason-review CLIs also work without setup.'
|