@hublo/sentinel 1.5.1-alpha.2 → 1.6.0-alpha.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.
@@ -70,6 +70,7 @@ import {
70
70
  readProjectPackageJson
71
71
  } from "./chunk-JRZI5IIN.js";
72
72
  import {
73
+ DEFAULT_ENV_LEVEL,
73
74
  PRESET_NAMES,
74
75
  REPORT_DIR,
75
76
  SCRIPTS,
@@ -80,7 +81,10 @@ import {
80
81
  extensionsOf,
81
82
  isPlainObject,
82
83
  overridableSettings
83
- } from "./chunk-WBDBRGLV.js";
84
+ } from "./chunk-7E6YTTAK.js";
85
+ import {
86
+ defineEnv
87
+ } from "./chunk-NAFI7OLI.js";
84
88
  import {
85
89
  globToRegExp
86
90
  } from "./chunk-O7REVMOC.js";
@@ -171,6 +175,67 @@ function palette(stream) {
171
175
  };
172
176
  }
173
177
 
178
+ // src/roles/env/presets/shared.json
179
+ var shared_default = {
180
+ readers: {
181
+ required: ["getOrFail", "requireEnv", "requireEnvVariable"],
182
+ optional: ["getOrDefault", "getEnvVariableOrDefault"]
183
+ },
184
+ scan: {
185
+ roots: ["."],
186
+ ext: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"],
187
+ excludeSuffixes: [
188
+ ".spec.ts",
189
+ ".test.ts",
190
+ ".spec.tsx",
191
+ ".test.tsx",
192
+ ".spec.js",
193
+ ".test.js",
194
+ ".spec.jsx",
195
+ ".test.jsx"
196
+ ],
197
+ excludeDirs: ["node_modules", "dist"]
198
+ },
199
+ references: {
200
+ roots: ["."],
201
+ ext: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".svelte", ".vue"],
202
+ excludeSuffixes: [],
203
+ excludeDirs: ["node_modules", "dist"]
204
+ },
205
+ referencePatterns: [
206
+ "process\\.env\\s*\\.\\s*([A-Za-z0-9_]+)",
207
+ "process\\.env\\[\\s*['\"`]([^'\"`]+)['\"`]\\s*\\]",
208
+ "import\\.meta\\.env\\s*\\.\\s*([A-Za-z0-9_]+)",
209
+ "import\\.meta\\.env\\[\\s*['\"`]([^'\"`]+)['\"`]\\s*\\]",
210
+ `import\\s*\\{([^}]*)\\}\\s*from\\s*['"]\\$env/static/(?:public|private)['"]`,
211
+ "\\benv\\.([A-Z][A-Z0-9_]*)",
212
+ "\\{([^{}]*)\\}\\s*=\\s*env\\b"
213
+ ],
214
+ secretNamePatterns: ["SECRET", "TOKEN", "PASSWORD", "PRIVATE_?KEY", "ACCESS_?KEY", "API_?KEY"],
215
+ publicPrefixes: ["VITE_", "PUBLIC_", "NEXT_PUBLIC_", "REACT_APP_"],
216
+ envFiles: ["**/.env", "**/.env.*", "**/*.env"],
217
+ contracts: ["**/*.env*.example"],
218
+ nodeVersionFile: ".node-version"
219
+ };
220
+
221
+ // src/roles/env/preset-data.ts
222
+ var data = shared_default;
223
+ var envDefaults = data;
224
+ function referencePatterns() {
225
+ return data.referencePatterns.map((source) => new RegExp(source, "g"));
226
+ }
227
+ function secretNamePatterns() {
228
+ return data.secretNamePatterns.map((source) => new RegExp(source, "i"));
229
+ }
230
+ var TARGET2 = "env";
231
+ var ENV_CONFIG_FILE = "env.config.mjs";
232
+ var REPORT_DIR2 = "env-report";
233
+ var ENV_SHAPES = ["console", "json", "markdown"];
234
+ var SHAPE_FILE = {
235
+ json: "env.json",
236
+ markdown: "env.md"
237
+ };
238
+
174
239
  // src/roles/build/nest/nx-target.ts
175
240
  import { existsSync, readFileSync } from "node:fs";
176
241
  import { join, relative } from "node:path";
@@ -1289,15 +1354,1044 @@ function registerBuild() {
1289
1354
  setDefaultRunner("dev", "vite");
1290
1355
  }
1291
1356
 
1357
+ // src/roles/env/adapters/conformance/conformance.adapter.ts
1358
+ import { existsSync as existsSync7, mkdirSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
1359
+ import { join as join10 } from "node:path";
1360
+
1361
+ // src/roles/env/collect.ts
1362
+ import { dirname, join as join8 } from "node:path/posix";
1363
+
1364
+ // src/roles/env/checks.ts
1365
+ var READER_VAR_NAME = String.raw`[A-Z0-9_]+`;
1366
+ var REFERENCED_NAME = /[A-Za-z_][A-Za-z0-9_]*/g;
1367
+ function lineAt(content, index) {
1368
+ let line = 1;
1369
+ for (let position = content.indexOf("\n"); position !== -1 && position < index; ) {
1370
+ line += 1;
1371
+ position = content.indexOf("\n", position + 1);
1372
+ }
1373
+ return line;
1374
+ }
1375
+ function readerRegex(readerName) {
1376
+ return new RegExp(String.raw`(?<!\w)${readerName}\(\s*[\`'"](${READER_VAR_NAME})[\`'"]`, "g");
1377
+ }
1378
+ function recordReads(reads, matchers, file) {
1379
+ for (const matcher of matchers) {
1380
+ for (const match of file.content.matchAll(matcher)) {
1381
+ const name = match[1];
1382
+ if (name === void 0) continue;
1383
+ const sites = reads.get(name);
1384
+ if (sites?.some((site2) => site2.path === file.path)) continue;
1385
+ const site = { path: file.path, line: lineAt(file.content, match.index) };
1386
+ if (sites) sites.push(site);
1387
+ else reads.set(name, [site]);
1388
+ }
1389
+ }
1390
+ }
1391
+ function deriveVars(files, readers) {
1392
+ const requiredMatchers = (readers.required ?? []).map(readerRegex);
1393
+ const optionalMatchers = (readers.optional ?? []).map(readerRegex);
1394
+ const required = /* @__PURE__ */ new Map();
1395
+ const optional = /* @__PURE__ */ new Map();
1396
+ for (const file of files) {
1397
+ recordReads(required, requiredMatchers, file);
1398
+ recordReads(optional, optionalMatchers, file);
1399
+ }
1400
+ return { required, optional };
1401
+ }
1402
+ var ENV_LINE = /^(?:export\s+)?([\w.-]+)\s*=(.*)$/;
1403
+ function isBlankValue(value) {
1404
+ const trimmed = value.trim();
1405
+ const quoted = /^(['"])(.*)\1$/.exec(trimmed);
1406
+ return (quoted ? quoted[2] : trimmed) === "";
1407
+ }
1408
+ function openQuote(value) {
1409
+ const trimmed = value.trim();
1410
+ const quote = trimmed[0];
1411
+ if (quote !== '"' && quote !== "'" && quote !== "`") return void 0;
1412
+ return trimmed.length > 1 && trimmed.endsWith(quote) ? void 0 : quote;
1413
+ }
1414
+ function parseEnvFile(content) {
1415
+ const entries = [];
1416
+ const lines = content.split("\n");
1417
+ let closing;
1418
+ for (const [index, rawLine] of lines.entries()) {
1419
+ const line = rawLine.trim();
1420
+ if (closing !== void 0) {
1421
+ if (line.endsWith(closing)) closing = void 0;
1422
+ continue;
1423
+ }
1424
+ if (line === "" || line.startsWith("#")) continue;
1425
+ const match = ENV_LINE.exec(line);
1426
+ if (!match) continue;
1427
+ const value = match[2] ?? "";
1428
+ entries.push({ key: match[1] ?? "", line: index + 1, blank: isBlankValue(value) });
1429
+ closing = openQuote(value);
1430
+ }
1431
+ return {
1432
+ entries,
1433
+ keys: entries.map((entry) => entry.key),
1434
+ blanked: entries.filter((entry) => entry.blank).map((entry) => entry.key),
1435
+ filled: entries.filter((entry) => !entry.blank).map((entry) => entry.key)
1436
+ };
1437
+ }
1438
+ function diffContract(derived, declared, externallyProvided = []) {
1439
+ const declaredKeys = new Set(declared);
1440
+ const external = new Set(externallyProvided);
1441
+ const unaccounted = [...derived.required.keys()].filter((name) => !declaredKeys.has(name) && !external.has(name)).sort();
1442
+ const optionalUndeclared = [...derived.optional.keys()].filter((name) => !declaredKeys.has(name)).sort();
1443
+ return { unaccounted, optionalUndeclared };
1444
+ }
1445
+ function findOrphanEnvFiles(envFiles, allow = []) {
1446
+ const allowed = new Set(allow);
1447
+ return envFiles.filter((path2) => !path2.endsWith(".example") && !allowed.has(path2)).sort();
1448
+ }
1449
+ function* referencedNames(content, patterns) {
1450
+ for (const pattern of patterns) {
1451
+ for (const match of content.matchAll(pattern)) {
1452
+ for (const [name] of (match[1] ?? "").matchAll(REFERENCED_NAME)) yield name;
1453
+ }
1454
+ }
1455
+ }
1456
+ function findReferencedVars(files, patterns) {
1457
+ const referenced = /* @__PURE__ */ new Map();
1458
+ for (const { path: path2, content } of files) {
1459
+ for (const name of referencedNames(content, patterns)) {
1460
+ const readers = referenced.get(name) ?? /* @__PURE__ */ new Set();
1461
+ readers.add(path2);
1462
+ referenced.set(name, readers);
1463
+ }
1464
+ }
1465
+ return referenced;
1466
+ }
1467
+ function findDeadVars(declaredKeys, referenced, allow = [], allowPatterns = []) {
1468
+ const alive = new Set(referenced);
1469
+ const allowed = new Set(allow);
1470
+ const isExternal = (key) => allowed.has(key) || allowPatterns.some((pattern) => pattern.test(key));
1471
+ return [...new Set(declaredKeys)].filter((key) => !alive.has(key) && !isExternal(key)).sort();
1472
+ }
1473
+ function auditEnvFileSecrets(parsed, options) {
1474
+ const secretFilled = parsed.entries.filter(
1475
+ (entry) => !entry.blank && options.secretPatterns.some((pattern) => pattern.test(entry.key))
1476
+ );
1477
+ const isPublic = (entry) => options.publicPrefixes.some((prefix) => entry.key.startsWith(prefix));
1478
+ return {
1479
+ mustBlank: secretFilled.filter((entry) => !isPublic(entry)),
1480
+ publicSecrets: secretFilled.filter(isPublic)
1481
+ };
1482
+ }
1483
+ function splitAllowlisted(locations, allowlist) {
1484
+ const allow = new Set(allowlist);
1485
+ const allowed = [];
1486
+ const flagged = [];
1487
+ for (const location of locations) (allow.has(location) ? allowed : flagged).push(location);
1488
+ return { allowed, flagged };
1489
+ }
1490
+ function isDockerfilePath(path2) {
1491
+ const name = path2.slice(path2.lastIndexOf("/") + 1);
1492
+ if (name.endsWith(".dockerignore") || name.endsWith(".md")) return false;
1493
+ return name === "dockerfile" || /^Dockerfile([.-][^/]+)?$/.test(name) || /\.[Dd]ockerfile$/.test(name);
1494
+ }
1495
+ function copySources(line) {
1496
+ const words = line.split(/\s+/).slice(1);
1497
+ const flags = words.findIndex((word) => !word.startsWith("--"));
1498
+ const rest = flags === -1 ? "" : words.slice(flags).join(" ");
1499
+ if (rest.startsWith("<<")) return [];
1500
+ if (rest.startsWith("[")) {
1501
+ try {
1502
+ const parsed = JSON.parse(rest);
1503
+ if (Array.isArray(parsed) && parsed.every((entry) => typeof entry === "string")) {
1504
+ return parsed.slice(0, -1);
1505
+ }
1506
+ } catch {
1507
+ }
1508
+ }
1509
+ return words.slice(flags === -1 ? words.length : flags).slice(0, -1).map((word) => /^(['"])(.*)\1$/.exec(word)?.[2] ?? word);
1510
+ }
1511
+ function parseDockerfile(content) {
1512
+ const froms = [];
1513
+ const contextCopySources = [];
1514
+ for (const [index, rawLine] of content.split("\n").entries()) {
1515
+ const line = rawLine.trim();
1516
+ const from = /^FROM\s+(\S+)/i.exec(line);
1517
+ if (from) {
1518
+ froms.push({ image: from[1] ?? "", line: index + 1 });
1519
+ continue;
1520
+ }
1521
+ if (/^COPY\s/i.test(line) && !/--from=/i.test(line)) {
1522
+ for (const source of copySources(line)) {
1523
+ if (!/[*?$]/.test(source)) contextCopySources.push({ source, line: index + 1 });
1524
+ }
1525
+ }
1526
+ }
1527
+ return { froms, contextCopySources };
1528
+ }
1529
+ function dockerNodeVersion(fromImage) {
1530
+ const match = /(?:^|\/)node:([0-9]+(?:\.[0-9]+){0,2})/.exec(fromImage);
1531
+ return match?.[1] ?? null;
1532
+ }
1533
+ function normalizeNodeVersion(raw) {
1534
+ const version = raw.trim().replace(/^v/i, "");
1535
+ return version === "" ? void 0 : version;
1536
+ }
1537
+ function nodeVersionAligned(dockerVersion, expected) {
1538
+ return expected === dockerVersion || expected.startsWith(`${dockerVersion}.`);
1539
+ }
1540
+ function dockerHardening(content) {
1541
+ const lines = content.split("\n").map((line) => line.trim());
1542
+ let lastFrom = -1;
1543
+ lines.forEach((line, index) => {
1544
+ if (/^FROM\s/i.test(line)) lastFrom = index;
1545
+ });
1546
+ const start = lastFrom === -1 ? 0 : lastFrom;
1547
+ const finalStage = lines.slice(start).map((text, offset) => ({ text, line: start + offset + 1 }));
1548
+ let user = null;
1549
+ let userLine = start + 1;
1550
+ for (const { text, line } of finalStage) {
1551
+ const match = /^USER\s+(\S+)/i.exec(text);
1552
+ if (match) {
1553
+ user = match[1] ?? null;
1554
+ userLine = line;
1555
+ }
1556
+ }
1557
+ const runsAsRoot = !user || /^(root|0)(:|$)/i.test(user);
1558
+ const ownsTree = finalStage.some(({ text }) => /chown\s+-R/i.test(text));
1559
+ const copiesWithoutChown = ownsTree ? [] : finalStage.filter(({ text }) => /^COPY\s/i.test(text) && !/--chown=/i.test(text)).map(({ text, line }) => ({ instruction: text.replace(/\s+/g, " "), line }));
1560
+ return { user, userLine, runsAsRoot, copiesWithoutChown };
1561
+ }
1562
+ function parseNxTargetRefs(content) {
1563
+ return [...content.matchAll(/\bnx run\s+([^\s:$]+):([^\s$]+)/g)].map((match) => ({
1564
+ project: match[1] ?? "",
1565
+ targetSpec: match[2] ?? "",
1566
+ line: lineAt(content, match.index)
1567
+ }));
1568
+ }
1569
+ function nxTargetCandidates(targetSpec) {
1570
+ const parts = targetSpec.split(":");
1571
+ const candidates = [targetSpec];
1572
+ if (parts.length > 1) candidates.push(parts.slice(0, -1).join(":"));
1573
+ return candidates;
1574
+ }
1575
+ function resolveApp(path2, projects) {
1576
+ let best = null;
1577
+ for (const project of projects) {
1578
+ if (path2 === project.root || path2.startsWith(`${project.root}/`)) {
1579
+ if (!best || project.root.length > best.root.length) best = project;
1580
+ }
1581
+ }
1582
+ return best ? best.name : null;
1583
+ }
1584
+
1585
+ // src/roles/env/repository.ts
1586
+ import { execFileSync } from "node:child_process";
1587
+ import { readFileSync as readFileSync6, statSync as statSync2 } from "node:fs";
1588
+ import { join as join7 } from "node:path";
1589
+ var MAX_BUFFER = 256 * 1024 * 1024;
1590
+ function trackedPaths(root) {
1591
+ const output = execFileSync("git", ["ls-files", "-z"], {
1592
+ cwd: root,
1593
+ encoding: "utf8",
1594
+ maxBuffer: MAX_BUFFER
1595
+ });
1596
+ return output.split("\0").filter(Boolean);
1597
+ }
1598
+ function loadNxTargets(root) {
1599
+ const nx = resolveBin(root, "nx");
1600
+ if (nx === void 0) return void 0;
1601
+ const runNx = (args) => execFileSync(nx, args, {
1602
+ cwd: root,
1603
+ encoding: "utf8",
1604
+ stdio: ["ignore", "pipe", "ignore"],
1605
+ maxBuffer: MAX_BUFFER,
1606
+ env: { ...process.env, NX_DAEMON: "false" }
1607
+ });
1608
+ try {
1609
+ const projects = new Set(JSON.parse(runNx(["show", "projects", "--json"])));
1610
+ const cache = /* @__PURE__ */ new Map();
1611
+ return (project) => {
1612
+ if (!projects.has(project)) return null;
1613
+ const known = cache.get(project);
1614
+ if (known) return known;
1615
+ const data2 = JSON.parse(runNx(["show", "project", project, "--json"]));
1616
+ const targets2 = new Set(Object.keys(data2.targets ?? {}));
1617
+ cache.set(project, targets2);
1618
+ return targets2;
1619
+ };
1620
+ } catch {
1621
+ return void 0;
1622
+ }
1623
+ }
1624
+ function openRepository(root) {
1625
+ let tracked;
1626
+ try {
1627
+ tracked = trackedPaths(root);
1628
+ } catch (error) {
1629
+ throw new Error(
1630
+ `git could not list the tracked files of ${root}: ${error instanceof Error ? error.message : String(error)}`,
1631
+ { cause: error }
1632
+ );
1633
+ }
1634
+ let nx = null;
1635
+ const hasNx = tracked.includes("nx.json");
1636
+ return {
1637
+ root,
1638
+ tracked,
1639
+ read(path2) {
1640
+ try {
1641
+ return readFileSync6(join7(root, path2), "utf8");
1642
+ } catch {
1643
+ return void 0;
1644
+ }
1645
+ },
1646
+ exists(path2) {
1647
+ try {
1648
+ statSync2(join7(root, path2));
1649
+ return true;
1650
+ } catch {
1651
+ return false;
1652
+ }
1653
+ },
1654
+ hasNx,
1655
+ nxTargets() {
1656
+ if (nx === null) nx = hasNx ? loadNxTargets(root) : void 0;
1657
+ return nx;
1658
+ }
1659
+ };
1660
+ }
1661
+ function loadProjects(view) {
1662
+ const byRoot = /* @__PURE__ */ new Map();
1663
+ for (const file of view.tracked) {
1664
+ const manifest = /(?:^|\/)(project|package)\.json$/.exec(file);
1665
+ if (!manifest) continue;
1666
+ const slash = file.lastIndexOf("/");
1667
+ if (slash === -1) continue;
1668
+ const root = file.slice(0, slash);
1669
+ const fromProjectJson = manifest[1] === "project";
1670
+ const existing = byRoot.get(root);
1671
+ if (existing && !(fromProjectJson && !existing.fromProjectJson)) continue;
1672
+ try {
1673
+ const name = JSON.parse(view.read(file) ?? "").name;
1674
+ if (typeof name === "string" && name !== "") byRoot.set(root, { name, fromProjectJson });
1675
+ } catch {
1676
+ }
1677
+ }
1678
+ return [...byRoot].map(([root, { name }]) => ({ root, name }));
1679
+ }
1680
+
1681
+ // src/roles/env/collect.ts
1682
+ var RULES = {
1683
+ "required-var-undeclared": {
1684
+ number: 1,
1685
+ severity: "blocking",
1686
+ help: "read by a required reader but declared in no contract: add it to the right .env*.example, or to externallyProvided if CI or a secret store provides it"
1687
+ },
1688
+ "optional-var-undeclared": {
1689
+ number: 1,
1690
+ severity: "info",
1691
+ help: "read by an optional reader and declared in no contract; a default applies"
1692
+ },
1693
+ "committed-env-file": {
1694
+ number: 2,
1695
+ severity: "blocking",
1696
+ help: "a real env file is committed: remove it from git (only .example contracts belong there; values come from a secret store)"
1697
+ },
1698
+ "var-not-in-app-contract": {
1699
+ number: 3,
1700
+ severity: "advisory",
1701
+ help: "read in this app but declared only in another app's contract: declare it in this app's own contract, or in a shared root one"
1702
+ },
1703
+ "dead-var": {
1704
+ number: 4,
1705
+ severity: "advisory",
1706
+ help: "declared but read nowhere in source: a candidate to remove, or add it to knownExternalVars if a tool or CI consumes it"
1707
+ },
1708
+ "public-secret-to-review": {
1709
+ number: 5,
1710
+ severity: "advisory",
1711
+ help: "a public-prefixed, secret-named variable carries a committed value: it ships to the browser by design, confirm it is genuinely public"
1712
+ },
1713
+ "secret-not-blanked": {
1714
+ number: 6,
1715
+ severity: "blocking",
1716
+ help: "a secret-named variable carries a committed value: blank it (leave the value empty) and move the value to a secret store"
1717
+ },
1718
+ "secret-allowlisted": {
1719
+ number: 6,
1720
+ severity: "advisory",
1721
+ help: "a secret-named variable carries a committed value, accepted by allowNonBlankedSecrets: confirm it stays non-secret"
1722
+ },
1723
+ "dead-copy-path": {
1724
+ number: 7,
1725
+ severity: "blocking",
1726
+ help: "this COPY source does not exist: remove the line or fix the path"
1727
+ },
1728
+ "nx-project-missing": {
1729
+ number: 8,
1730
+ severity: "blocking",
1731
+ help: "this Dockerfile runs an nx project that does not exist: fix or remove the reference"
1732
+ },
1733
+ "nx-target-missing": {
1734
+ number: 8,
1735
+ severity: "blocking",
1736
+ help: "this Dockerfile runs an nx target the project does not have: fix or remove the reference"
1737
+ },
1738
+ "node-version-mismatch": {
1739
+ number: 9,
1740
+ severity: "blocking",
1741
+ help: "the FROM node version differs from .node-version: align the tag"
1742
+ },
1743
+ "dockerfile-runs-as-root": {
1744
+ number: 10,
1745
+ severity: "blocking",
1746
+ help: "the final stage runs as root: set a non-root USER (USER node, or USER 1000:1000) so a compromise cannot tamper with the running image"
1747
+ },
1748
+ "copy-not-owned": {
1749
+ number: 10,
1750
+ severity: "blocking",
1751
+ help: "this COPY into the runtime stage has no --chown, so it lands root-owned under a non-root USER: add --chown (node:node, or 1000:1000 with --link) or a RUN chown -R"
1752
+ }
1753
+ };
1754
+ var RULE_ORDER = Object.keys(RULES);
1755
+ function inScope(path2, scope) {
1756
+ const underRoot = scope.roots.some((root) => {
1757
+ const cleaned = root.replace(/^\.\//, "").replace(/\/+$/, "");
1758
+ return cleaned === "." || cleaned === "" || path2.startsWith(`${cleaned}/`);
1759
+ });
1760
+ if (!underRoot) return false;
1761
+ const segments = path2.split("/");
1762
+ const name = segments.pop() ?? "";
1763
+ if (segments.some((segment) => scope.excludeDirs.includes(segment))) return false;
1764
+ return scope.ext.some((suffix) => name.endsWith(suffix)) && !scope.excludeSuffixes.some((suffix) => name.endsWith(suffix));
1765
+ }
1766
+ function* sourcesOf(view, scope) {
1767
+ for (const path2 of view.tracked) {
1768
+ if (!inScope(path2, scope)) continue;
1769
+ const content = view.read(path2);
1770
+ if (content !== void 0) yield { path: path2, content };
1771
+ }
1772
+ }
1773
+ function missingRoots(view, settings) {
1774
+ const roots = [...settings.scan.roots, ...settings.references.roots];
1775
+ return [
1776
+ ...new Set(
1777
+ roots.filter((root) => {
1778
+ const cleaned = root.replace(/^\.\//, "").replace(/\/+$/, "");
1779
+ if (cleaned === "." || cleaned === "") return false;
1780
+ return !view.tracked.some((path2) => path2.startsWith(`${cleaned}/`));
1781
+ })
1782
+ )
1783
+ ];
1784
+ }
1785
+ function finding(rule, fields) {
1786
+ const { name, file, line, detail, readIn, app } = fields;
1787
+ return {
1788
+ rule,
1789
+ number: RULES[rule].number,
1790
+ severity: RULES[rule].severity,
1791
+ name,
1792
+ ...file === void 0 ? {} : { file },
1793
+ ...line === void 0 ? {} : { line },
1794
+ ...detail === void 0 ? {} : { detail },
1795
+ ...readIn === void 0 ? {} : { readIn },
1796
+ ...app === void 0 ? {} : { app }
1797
+ };
1798
+ }
1799
+ function byReportOrder(left, right) {
1800
+ return RULE_ORDER.indexOf(left.rule) - RULE_ORDER.indexOf(right.rule) || (left.file ?? "").localeCompare(right.file ?? "") || (left.line ?? 0) - (right.line ?? 0) || left.name.localeCompare(right.name);
1801
+ }
1802
+ function readEnvFiles(view, settings) {
1803
+ const envGlobs = settings.envFiles.map(globToRegExp);
1804
+ const contractGlobs = settings.contracts.map(globToRegExp);
1805
+ const envFiles = view.tracked.filter((path2) => envGlobs.some((glob) => glob.test(path2)));
1806
+ const contracts = view.tracked.filter((path2) => contractGlobs.some((glob) => glob.test(path2)));
1807
+ const parsed = /* @__PURE__ */ new Map();
1808
+ for (const path2 of /* @__PURE__ */ new Set([...envFiles, ...contracts])) {
1809
+ const content = view.read(path2);
1810
+ if (content !== void 0) parsed.set(path2, parseEnvFile(content));
1811
+ }
1812
+ return { envFiles, contracts, parsed };
1813
+ }
1814
+ function secretFindings(parsed, settings) {
1815
+ const options = {
1816
+ secretPatterns: settings.secretNamePatterns,
1817
+ publicPrefixes: settings.publicPrefixes
1818
+ };
1819
+ const findings = [];
1820
+ const mustBlank = /* @__PURE__ */ new Map();
1821
+ for (const [file, content] of parsed) {
1822
+ const audit = auditEnvFileSecrets(content, options);
1823
+ for (const entry of audit.publicSecrets) {
1824
+ findings.push(finding("public-secret-to-review", { name: entry.key, file, line: entry.line }));
1825
+ }
1826
+ for (const entry of audit.mustBlank) {
1827
+ mustBlank.set(`${file}:${entry.key}`, { file, line: entry.line, name: entry.key });
1828
+ }
1829
+ }
1830
+ const { allowed, flagged } = splitAllowlisted(
1831
+ [...mustBlank.keys()],
1832
+ settings.allowNonBlankedSecrets
1833
+ );
1834
+ for (const location of flagged) {
1835
+ const entry = mustBlank.get(location);
1836
+ if (entry) findings.push(finding("secret-not-blanked", entry));
1837
+ }
1838
+ for (const location of allowed) {
1839
+ const entry = mustBlank.get(location);
1840
+ if (entry) findings.push(finding("secret-allowlisted", entry));
1841
+ }
1842
+ return findings;
1843
+ }
1844
+ function allReadNames(derived, referenced) {
1845
+ return /* @__PURE__ */ new Set([...referenced.keys(), ...derived.required.keys(), ...derived.optional.keys()]);
1846
+ }
1847
+ function misplacedFindings(view, settings, derived, referenced, contracts, declared) {
1848
+ const projects = loadProjects(view);
1849
+ const readsByApp = /* @__PURE__ */ new Map();
1850
+ const addRead = (name, path2) => {
1851
+ const app = resolveApp(path2, projects);
1852
+ if (!app) return;
1853
+ const reads = readsByApp.get(app) ?? /* @__PURE__ */ new Map();
1854
+ if (!reads.has(name)) reads.set(name, path2);
1855
+ readsByApp.set(app, reads);
1856
+ };
1857
+ for (const [name, sites] of derived.required) for (const site of sites) addRead(name, site.path);
1858
+ for (const [name, paths] of referenced) for (const path2 of paths) addRead(name, path2);
1859
+ const shared = /* @__PURE__ */ new Set();
1860
+ const keysByApp = /* @__PURE__ */ new Map();
1861
+ for (const { path: path2, parsed } of contracts) {
1862
+ const app = resolveApp(path2, projects);
1863
+ const keys = app === null ? shared : keysByApp.get(app) ?? /* @__PURE__ */ new Set();
1864
+ for (const key of parsed.keys) keys.add(key);
1865
+ if (app !== null) keysByApp.set(app, keys);
1866
+ }
1867
+ const external = new Set(settings.externallyProvided);
1868
+ const findings = [];
1869
+ for (const [app, reads] of readsByApp) {
1870
+ const own = keysByApp.get(app);
1871
+ if (!own) continue;
1872
+ for (const [name, path2] of reads) {
1873
+ if (declared.has(name) && !own.has(name) && !shared.has(name) && !external.has(name)) {
1874
+ findings.push(
1875
+ finding("var-not-in-app-contract", {
1876
+ name,
1877
+ file: path2,
1878
+ app,
1879
+ detail: `read in ${app}`
1880
+ })
1881
+ );
1882
+ }
1883
+ }
1884
+ }
1885
+ return findings;
1886
+ }
1887
+ function codeFindings(view, settings, contracts) {
1888
+ const derived = deriveVars(sourcesOf(view, settings.scan), settings.readers);
1889
+ const inventory = {
1890
+ required: [...derived.required].map(([name, sites]) => ({ name, files: sites.length })).sort((left, right) => left.name.localeCompare(right.name)),
1891
+ optional: [...derived.optional.keys()].sort(),
1892
+ contracts: contracts.map(({ path: path2, parsed }) => ({ path: path2, keys: parsed.keys.length }))
1893
+ };
1894
+ const counts = { required: derived.required.size, optional: derived.optional.size };
1895
+ if (contracts.length === 0) return { findings: [], inventory, counts, compared: false };
1896
+ const declaredAt = /* @__PURE__ */ new Map();
1897
+ for (const { path: path2, parsed } of contracts) {
1898
+ for (const entry of parsed.entries) {
1899
+ if (!declaredAt.has(entry.key)) declaredAt.set(entry.key, { file: path2, line: entry.line });
1900
+ }
1901
+ }
1902
+ const declared = new Set(declaredAt.keys());
1903
+ const { unaccounted, optionalUndeclared } = diffContract(
1904
+ derived,
1905
+ declared,
1906
+ settings.externallyProvided
1907
+ );
1908
+ const referenced = findReferencedVars(
1909
+ sourcesOf(view, settings.references),
1910
+ settings.referencePatterns
1911
+ );
1912
+ const dead = findDeadVars(
1913
+ declared,
1914
+ allReadNames(derived, referenced),
1915
+ settings.knownExternalVars,
1916
+ settings.knownExternalPatterns
1917
+ );
1918
+ const findings = [
1919
+ ...unaccounted.map((name) => {
1920
+ const sites = derived.required.get(name) ?? [];
1921
+ return finding("required-var-undeclared", {
1922
+ name,
1923
+ file: sites[0]?.path,
1924
+ line: sites[0]?.line,
1925
+ readIn: sites.map((site) => site.path)
1926
+ });
1927
+ }),
1928
+ ...misplacedFindings(view, settings, derived, referenced, contracts, declared),
1929
+ ...dead.map((name) => finding("dead-var", { name, ...declaredAt.get(name) })),
1930
+ ...optionalUndeclared.map((name) => {
1931
+ const site = derived.optional.get(name)?.[0];
1932
+ return finding("optional-var-undeclared", { name, file: site?.path, line: site?.line });
1933
+ })
1934
+ ];
1935
+ return { findings, inventory, counts, compared: true };
1936
+ }
1937
+ function dockerFindings(view, settings) {
1938
+ const excluded = (path2) => settings.docker.excludePaths.some((prefix) => path2.startsWith(prefix));
1939
+ const dockerfiles = view.tracked.filter((path2) => isDockerfilePath(path2) && !excluded(path2));
1940
+ const rawNodeVersion = view.read(settings.nodeVersionFile);
1941
+ const expected = rawNodeVersion === void 0 ? void 0 : normalizeNodeVersion(rawNodeVersion);
1942
+ const findings = [];
1943
+ const notes = [];
1944
+ let uncheckedNxRefs = 0;
1945
+ let nxUnavailable = false;
1946
+ for (const file of dockerfiles) {
1947
+ const content = view.read(file);
1948
+ if (content === void 0) continue;
1949
+ const { froms, contextCopySources } = parseDockerfile(content);
1950
+ for (const { image, line } of froms) {
1951
+ const version = dockerNodeVersion(image);
1952
+ if (version && expected && !nodeVersionAligned(version, expected)) {
1953
+ findings.push(
1954
+ finding("node-version-mismatch", {
1955
+ name: `node:${version}`,
1956
+ file,
1957
+ line,
1958
+ detail: `expected ${expected} (${settings.nodeVersionFile})`
1959
+ })
1960
+ );
1961
+ }
1962
+ }
1963
+ for (const { source, line } of contextCopySources) {
1964
+ if (!view.exists(join8(".", source)) && !view.exists(join8(dirname(file), source))) {
1965
+ findings.push(finding("dead-copy-path", { name: source, file, line }));
1966
+ }
1967
+ }
1968
+ const { runsAsRoot, user, userLine, copiesWithoutChown } = dockerHardening(content);
1969
+ if (runsAsRoot && !settings.docker.allowRoot.includes(file)) {
1970
+ findings.push(
1971
+ finding("dockerfile-runs-as-root", {
1972
+ name: user ? `USER ${user}` : "no USER set",
1973
+ file,
1974
+ line: userLine
1975
+ })
1976
+ );
1977
+ }
1978
+ for (const { instruction, line } of copiesWithoutChown) {
1979
+ findings.push(finding("copy-not-owned", { name: instruction, file, line }));
1980
+ }
1981
+ const refs = parseNxTargetRefs(content);
1982
+ if (refs.length === 0) continue;
1983
+ const lookup = view.hasNx ? view.nxTargets() : void 0;
1984
+ if (lookup === void 0) {
1985
+ uncheckedNxRefs += refs.length;
1986
+ nxUnavailable ||= view.hasNx;
1987
+ continue;
1988
+ }
1989
+ for (const { project, targetSpec, line } of refs) {
1990
+ const targets2 = lookup(project);
1991
+ if (targets2 === null) {
1992
+ findings.push(finding("nx-project-missing", { name: project, file, line }));
1993
+ } else if (!nxTargetCandidates(targetSpec).some((target) => targets2.has(target))) {
1994
+ findings.push(
1995
+ finding("nx-target-missing", { name: `${project}:${targetSpec}`, file, line })
1996
+ );
1997
+ }
1998
+ }
1999
+ }
2000
+ if (dockerfiles.length === 0)
2001
+ notes.push("no Dockerfile is tracked: rules 7 to 10 had nothing to check");
2002
+ if (dockerfiles.length > 0 && expected === void 0) {
2003
+ notes.push(
2004
+ `no ${settings.nodeVersionFile} at the repository root: rule 9 had no version to compare the FROM tags with`
2005
+ );
2006
+ }
2007
+ if (!view.hasNx) {
2008
+ notes.push(
2009
+ uncheckedNxRefs > 0 ? `not an nx workspace (no nx.json): rule 8 skipped, ${uncheckedNxRefs} \`nx run\` reference(s) not checked` : "not an nx workspace (no nx.json): rule 8 skipped"
2010
+ );
2011
+ } else if (nxUnavailable) {
2012
+ notes.push(
2013
+ `the repository's own nx could not run (not installed?): rule 8 skipped, ${uncheckedNxRefs} \`nx run\` reference(s) not checked`
2014
+ );
2015
+ }
2016
+ return { findings, notes, dockerfiles: dockerfiles.length, nodeVersion: expected ?? null };
2017
+ }
2018
+ function collectEnvReport(view, settings, level, config) {
2019
+ const missing = missingRoots(view, settings);
2020
+ if (missing.length > 0) {
2021
+ throw new Error(
2022
+ `${config ?? "the env settings"}: scan root(s) holding no tracked file: ${missing.join(", ")}`
2023
+ );
2024
+ }
2025
+ const { envFiles, contracts, parsed } = readEnvFiles(view, settings);
2026
+ const contractFiles = contracts.flatMap((path2) => {
2027
+ const content = parsed.get(path2);
2028
+ return content ? [{ path: path2, parsed: content }] : [];
2029
+ });
2030
+ const code = codeFindings(view, settings, contractFiles);
2031
+ const docker = dockerFindings(view, settings);
2032
+ const orphans = findOrphanEnvFiles(envFiles, settings.allowedCommittedEnvFiles);
2033
+ const notes = [];
2034
+ if (!code.compared) {
2035
+ notes.push(
2036
+ `no contract is committed (${settings.contracts.join(", ")}): rules 1, 3 and 4 had nothing to compare the code with`
2037
+ );
2038
+ }
2039
+ if (code.counts.required === 0) {
2040
+ notes.push(
2041
+ `no required reader call found (${settings.readers.required.join(", ")}): rule 1 had nothing to check; name the repository's own helper in readers.required`
2042
+ );
2043
+ }
2044
+ notes.push(...docker.notes);
2045
+ return {
2046
+ level,
2047
+ config,
2048
+ scanned: settings.scan.roots,
2049
+ counts: {
2050
+ required: code.counts.required,
2051
+ optional: code.counts.optional,
2052
+ contracts: contractFiles.length,
2053
+ envFiles: envFiles.length,
2054
+ dockerfiles: docker.dockerfiles
2055
+ },
2056
+ nodeVersion: docker.nodeVersion,
2057
+ findings: [
2058
+ ...code.findings,
2059
+ ...orphans.map((file) => finding("committed-env-file", { name: file, file })),
2060
+ ...secretFindings(parsed, settings),
2061
+ ...docker.findings
2062
+ ].sort(byReportOrder),
2063
+ notes,
2064
+ inventory: code.inventory
2065
+ };
2066
+ }
2067
+ function blockingFindings(report) {
2068
+ return report.findings.filter((entry) => entry.severity === "blocking");
2069
+ }
2070
+
2071
+ // src/roles/env/configs/resolve.ts
2072
+ import { existsSync as existsSync6 } from "node:fs";
2073
+ import { join as join9 } from "node:path";
2074
+ import { pathToFileURL } from "node:url";
2075
+ function explainUnreadableEnvConfig(error) {
2076
+ const message = error instanceof Error ? error.message : String(error);
2077
+ const code = error.code;
2078
+ const missingPackage = (code === "ERR_PACKAGE_PATH_NOT_EXPORTED" || code === "ERR_MODULE_NOT_FOUND") && message.includes("@hublo/sentinel");
2079
+ if (!missingPackage) return message;
2080
+ return `${message}
2081
+ This repository cannot import @hublo/sentinel/env: it does not install sentinel, or installs a release without the env role. Export the same object without defineEnv (export default { ... }), which sentinel validates the same way.`;
2082
+ }
2083
+ function unique(values) {
2084
+ return [...new Set(values)];
2085
+ }
2086
+ function scopeOf(base, input) {
2087
+ return {
2088
+ roots: input?.roots ?? base.roots,
2089
+ ext: input?.ext ?? base.ext,
2090
+ excludeSuffixes: input?.excludeSuffixes ?? base.excludeSuffixes,
2091
+ excludeDirs: input?.excludeDirs ?? base.excludeDirs
2092
+ };
2093
+ }
2094
+ function asPattern(entry) {
2095
+ return entry instanceof RegExp ? entry : new RegExp(entry);
2096
+ }
2097
+ function resolveEnvSettings(input) {
2098
+ return {
2099
+ readers: {
2100
+ required: unique([...envDefaults.readers.required, ...input.readers?.required ?? []]),
2101
+ optional: unique([...envDefaults.readers.optional, ...input.readers?.optional ?? []])
2102
+ },
2103
+ scan: scopeOf(envDefaults.scan, input.scan),
2104
+ references: scopeOf(envDefaults.references, input.references),
2105
+ referencePatterns: referencePatterns(),
2106
+ secretNamePatterns: secretNamePatterns(),
2107
+ publicPrefixes: [...envDefaults.publicPrefixes],
2108
+ envFiles: unique([...envDefaults.envFiles, ...input.envFiles ?? []]),
2109
+ contracts: unique([...envDefaults.contracts, ...input.contracts ?? []]),
2110
+ nodeVersionFile: envDefaults.nodeVersionFile,
2111
+ externallyProvided: input.externallyProvided ?? [],
2112
+ knownExternalVars: input.knownExternalVars ?? [],
2113
+ knownExternalPatterns: (input.knownExternalPatterns ?? []).map(asPattern),
2114
+ allowNonBlankedSecrets: input.allowNonBlankedSecrets ?? [],
2115
+ allowedCommittedEnvFiles: input.allowedCommittedEnvFiles ?? [],
2116
+ docker: {
2117
+ allowRoot: input.docker?.allowRoot ?? [],
2118
+ excludePaths: input.docker?.excludePaths ?? []
2119
+ }
2120
+ };
2121
+ }
2122
+ async function loadEnvConfig(root) {
2123
+ const path2 = join9(root, ENV_CONFIG_FILE);
2124
+ if (!existsSync6(path2)) {
2125
+ return { settings: resolveEnvSettings({}), from: null, unreadable: null };
2126
+ }
2127
+ try {
2128
+ const loaded = await import(pathToFileURL(path2).href);
2129
+ const declared = defineEnv(loaded.default ?? {});
2130
+ return { settings: resolveEnvSettings(declared), from: ENV_CONFIG_FILE, unreadable: null };
2131
+ } catch (error) {
2132
+ return {
2133
+ settings: resolveEnvSettings({}),
2134
+ from: null,
2135
+ unreadable: `${ENV_CONFIG_FILE}: ${explainUnreadableEnvConfig(error)}`
2136
+ };
2137
+ }
2138
+ }
2139
+
2140
+ // src/roles/env/report.ts
2141
+ var LEVEL_MEANING = {
2142
+ info: "report only",
2143
+ warning: "report, and annotate in GitHub Actions; never fails",
2144
+ error: "fails on a blocking finding (rules 1, 2, 6, 7 to 10)"
2145
+ };
2146
+ function locationOf(entry) {
2147
+ if (entry.file === void 0) return "";
2148
+ return entry.line === void 0 ? entry.file : `${entry.file}:${entry.line}`;
2149
+ }
2150
+ function groupByRule(findings) {
2151
+ const groups = /* @__PURE__ */ new Map();
2152
+ for (const entry of findings) {
2153
+ const group = groups.get(entry.rule) ?? [];
2154
+ group.push(entry);
2155
+ groups.set(entry.rule, group);
2156
+ }
2157
+ return groups;
2158
+ }
2159
+ function describe(entry) {
2160
+ const where = locationOf(entry);
2161
+ const subject = entry.rule === "committed-env-file" ? "" : entry.name;
2162
+ const more = entry.readIn !== void 0 && entry.readIn.length > 1 ? ` (read in ${entry.readIn.length} files)` : entry.detail ? ` (${entry.detail})` : "";
2163
+ return [where, subject].filter(Boolean).join(" ") + more;
2164
+ }
2165
+ function verdictOf(report) {
2166
+ const blocking = blockingFindings(report).length;
2167
+ if (blocking === 0) return "no blocking finding";
2168
+ const counted = `${blocking} blocking finding${blocking === 1 ? "" : "s"}`;
2169
+ return report.level === "error" ? `${counted}: failing` : `${counted}: reported, not failing`;
2170
+ }
2171
+ function headline(report) {
2172
+ const { counts } = report;
2173
+ return `${counts.envFiles} env file(s), ${counts.contracts} contract(s), ${counts.required} required and ${counts.optional} optional variable(s) read, ${counts.dockerfiles} Dockerfile(s)` + (report.nodeVersion ? `, node ${report.nodeVersion}` : "");
2174
+ }
2175
+ function renderConsole(report, palette2) {
2176
+ const lines = [];
2177
+ lines.push(
2178
+ `${palette2.strong("env")} ${palette2.dim(`(level ${report.level}: ${LEVEL_MEANING[report.level]})`)}`
2179
+ );
2180
+ lines.push(` ${headline(report)}`);
2181
+ lines.push(
2182
+ ` ${palette2.dim(`settings: ${report.config ?? "sentinel defaults (no env.config.mjs)"}`)}`
2183
+ );
2184
+ const listed = report.findings.filter((entry) => entry.severity !== "info");
2185
+ const informational = report.findings.length - listed.length;
2186
+ for (const [rule, entries] of groupByRule(listed)) {
2187
+ const meta = RULES[rule];
2188
+ const mark = meta.severity === "blocking" ? palette2.fail("\u2717") : palette2.warn("!");
2189
+ lines.push("");
2190
+ lines.push(
2191
+ ` ${mark} ${palette2.strong(`[${rule}]`)} ${palette2.dim(`rule ${meta.number}, ${meta.severity}, ${entries.length}`)}`
2192
+ );
2193
+ lines.push(` ${palette2.dim(meta.help)}`);
2194
+ for (const entry of entries) lines.push(` ${describe(entry)}`);
2195
+ }
2196
+ if (informational > 0) {
2197
+ lines.push("");
2198
+ lines.push(
2199
+ ` ${palette2.dim(`info: ${informational} optional variable(s) in no contract (a default applies); listed in --json`)}`
2200
+ );
2201
+ }
2202
+ for (const note of report.notes) lines.push(` ${palette2.dim(`note: ${note}`)}`);
2203
+ lines.push("");
2204
+ const blocking = blockingFindings(report).length;
2205
+ const verdict = verdictOf(report);
2206
+ lines.push(
2207
+ ` ${blocking === 0 ? palette2.ok(verdict) : report.level === "error" ? palette2.fail(verdict) : palette2.warn(verdict)}`
2208
+ );
2209
+ return `${lines.join("\n")}
2210
+ `;
2211
+ }
2212
+ function cell(text) {
2213
+ return text.replace(/\|/g, "\\|");
2214
+ }
2215
+ function renderMarkdown(report) {
2216
+ const lines = ["<!-- sentinel-env-report -->", "## Env conformance", ""];
2217
+ lines.push(`**Level:** \`${report.level}\` (${LEVEL_MEANING[report.level]}) `);
2218
+ lines.push(`**Scanned:** ${headline(report)} `);
2219
+ lines.push(`**Settings:** ${report.config ? `\`${report.config}\`` : "sentinel defaults"}`);
2220
+ lines.push("");
2221
+ const listed = report.findings.filter((entry) => entry.severity !== "info");
2222
+ const groups = groupByRule(listed);
2223
+ if (groups.size === 0) {
2224
+ lines.push("No finding.");
2225
+ } else {
2226
+ lines.push("| rule | finding | severity | count |", "| --- | --- | --- | --- |");
2227
+ for (const [rule, entries] of groups) {
2228
+ const meta = RULES[rule];
2229
+ lines.push(`| ${meta.number} | \`${rule}\` | ${meta.severity} | ${entries.length} |`);
2230
+ }
2231
+ for (const [rule, entries] of groups) {
2232
+ lines.push("", `<details><summary><code>${rule}</code>: ${entries.length}</summary>`, "");
2233
+ lines.push(cell(RULES[rule].help), "");
2234
+ for (const entry of entries) lines.push(`- \`${cell(describe(entry))}\``);
2235
+ lines.push("", "</details>");
2236
+ }
2237
+ }
2238
+ if (report.notes.length > 0) {
2239
+ lines.push("");
2240
+ for (const note of report.notes) lines.push(`> ${note}`);
2241
+ }
2242
+ lines.push("", `**Verdict:** ${verdictOf(report)}`);
2243
+ return `${lines.join("\n")}
2244
+ `;
2245
+ }
2246
+ function reportDocument(report) {
2247
+ return {
2248
+ level: report.level,
2249
+ verdict: verdictOf(report),
2250
+ blocking: blockingFindings(report).length,
2251
+ config: report.config,
2252
+ scanned: report.scanned,
2253
+ counts: report.counts,
2254
+ nodeVersion: report.nodeVersion,
2255
+ notes: report.notes,
2256
+ findings: report.findings,
2257
+ inventory: report.inventory
2258
+ };
2259
+ }
2260
+ function renderJson(report) {
2261
+ return `${JSON.stringify(reportDocument(report), null, 2)}
2262
+ `;
2263
+ }
2264
+ function escapeData(text) {
2265
+ return text.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
2266
+ }
2267
+ function escapeProperty(text) {
2268
+ return escapeData(text).replace(/:/g, "%3A").replace(/,/g, "%2C");
2269
+ }
2270
+ function renderAnnotations(report) {
2271
+ if (report.level === "info") return [];
2272
+ return report.findings.filter((entry) => entry.severity !== "info").map((entry) => {
2273
+ const command = report.level === "error" && entry.severity === "blocking" ? "error" : "warning";
2274
+ const properties = [
2275
+ entry.file === void 0 ? void 0 : `file=${escapeProperty(entry.file)}`,
2276
+ entry.line === void 0 ? void 0 : `line=${entry.line}`,
2277
+ `title=${escapeProperty(`env rule ${entry.number}: ${entry.rule}`)}`
2278
+ ].filter(Boolean);
2279
+ const message = `${entry.name}${entry.detail ? ` (${entry.detail})` : ""}: ${RULES[entry.rule].help}`;
2280
+ return `::${command} ${properties.join(",")}::${escapeData(message)}`;
2281
+ });
2282
+ }
2283
+
2284
+ // src/roles/env/adapters/conformance/conformance.adapter.ts
2285
+ function shapesOf(output) {
2286
+ const asked = [
2287
+ ...new Set(
2288
+ (output ?? "console").split(",").map((one) => one.trim()).filter((one) => one !== "")
2289
+ )
2290
+ ];
2291
+ return {
2292
+ known: asked.filter((one) => ENV_SHAPES.includes(one)),
2293
+ unknown: asked.filter((one) => !ENV_SHAPES.includes(one))
2294
+ };
2295
+ }
2296
+ function writeShapes(root, report, shapes) {
2297
+ const files = shapes.filter((shape) => shape !== "console");
2298
+ if (files.length === 0) return [];
2299
+ const directory = join10(root, REPORT_DIR2);
2300
+ mkdirSync(directory, { recursive: true });
2301
+ for (const [shape, name] of Object.entries(SHAPE_FILE)) {
2302
+ const path2 = join10(directory, name);
2303
+ if (!files.includes(shape) && existsSync7(path2)) unlinkSync(path2);
2304
+ }
2305
+ return files.map((shape) => {
2306
+ const name = SHAPE_FILE[shape];
2307
+ writeFileSync2(
2308
+ join10(directory, name),
2309
+ shape === "json" ? renderJson(report) : renderMarkdown(report)
2310
+ );
2311
+ return `${REPORT_DIR2}/${name}`;
2312
+ });
2313
+ }
2314
+ var ConformanceAdapter = class extends BaseAdapter {
2315
+ target = TARGET2;
2316
+ runner = "conformance";
2317
+ /** A repository has no stack, so every preset applies: the role never reads one. */
2318
+ appliesTo(_preset) {
2319
+ return true;
2320
+ }
2321
+ /** Nothing to adopt: the role reads the repository and writes no config of its own. */
2322
+ plan() {
2323
+ return {
2324
+ operations: [],
2325
+ blocked: "env has no --init: it writes no config. An optional env.config.mjs at the repository root declares exceptions; run it with `sentinel --run --env`."
2326
+ };
2327
+ }
2328
+ /** `--run`: check, print the report, write the asked files, annotate. */
2329
+ async run(ctx) {
2330
+ return this.check(ctx, true);
2331
+ }
2332
+ /** `--run --json`: the same check, for a machine. Prints nothing on stdout. */
2333
+ async report(ctx) {
2334
+ return this.check(ctx, false);
2335
+ }
2336
+ say(message) {
2337
+ process.stderr.write(`sentinel env: ${message}
2338
+ `);
2339
+ }
2340
+ async check(ctx, printing) {
2341
+ const level = ctx.envLevel ?? DEFAULT_ENV_LEVEL;
2342
+ const { known, unknown } = shapesOf(ctx.output);
2343
+ if (unknown.length > 0) {
2344
+ this.say(
2345
+ `writes ${ENV_SHAPES.join(", ")}; not ${unknown.join(", ")}, which another role may write.`
2346
+ );
2347
+ }
2348
+ const { settings, from, unreadable } = await loadEnvConfig(ctx.cwd);
2349
+ if (unreadable) {
2350
+ this.say(unreadable);
2351
+ return { ok: false, code: 1 };
2352
+ }
2353
+ let report;
2354
+ try {
2355
+ report = collectEnvReport(openRepository(ctx.cwd), settings, level, from);
2356
+ } catch (error) {
2357
+ this.say(error instanceof Error ? error.message : String(error));
2358
+ return { ok: false, code: 1 };
2359
+ }
2360
+ if (printing) {
2361
+ const written = writeShapes(ctx.cwd, report, known);
2362
+ process.stdout.write(renderConsole(report, palette(process.stdout)));
2363
+ if (written.length > 0) process.stdout.write(`
2364
+ written: ${written.join(", ")}
2365
+ `);
2366
+ }
2367
+ if (ctx.githubActions) {
2368
+ const annotations = renderAnnotations(report);
2369
+ if (annotations.length > 0) {
2370
+ const stream = printing ? process.stdout : process.stderr;
2371
+ stream.write(`${annotations.join("\n")}
2372
+ `);
2373
+ }
2374
+ }
2375
+ const failing = level === "error" && blockingFindings(report).length > 0;
2376
+ return { ok: !failing, code: failing ? 1 : 0, metrics: reportDocument(report) };
2377
+ }
2378
+ };
2379
+
2380
+ // src/roles/env/register.ts
2381
+ function registerEnv() {
2382
+ register(new ConformanceAdapter());
2383
+ setDefaultRunner("env", "conformance");
2384
+ }
2385
+
1292
2386
  // src/roles/format/adapters/oxfmt/oxfmt.adapter.ts
1293
2387
  import { spawnSync as spawnSync3 } from "node:child_process";
1294
- import { existsSync as existsSync8, readFileSync as readFileSync9 } from "node:fs";
1295
- import { join as join11 } from "node:path";
2388
+ import { existsSync as existsSync10, readFileSync as readFileSync10 } from "node:fs";
2389
+ import { join as join15 } from "node:path";
1296
2390
  import { isDeepStrictEqual } from "node:util";
1297
2391
 
1298
2392
  // src/core/config/has-source.ts
1299
2393
  import { readdirSync } from "node:fs";
1300
- import { extname, join as join7, relative as relative2 } from "node:path";
2394
+ import { extname, join as join11, relative as relative2 } from "node:path";
1301
2395
  var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
1302
2396
  "node_modules",
1303
2397
  "dist",
@@ -1348,7 +2442,7 @@ function hasSourceFiles(cwd, extensions) {
1348
2442
  }
1349
2443
  for (const entry of entries) {
1350
2444
  if (entry.isDirectory()) {
1351
- if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(join7(dir, entry.name));
2445
+ if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(join11(dir, entry.name));
1352
2446
  continue;
1353
2447
  }
1354
2448
  if (wanted.has(extname(entry.name))) return true;
@@ -1369,7 +2463,7 @@ function listSourceFiles(cwd, extensions) {
1369
2463
  continue;
1370
2464
  }
1371
2465
  for (const entry of entries) {
1372
- const full = join7(dir, entry.name);
2466
+ const full = join11(dir, entry.name);
1373
2467
  if (entry.isDirectory()) {
1374
2468
  if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(full);
1375
2469
  continue;
@@ -1388,8 +2482,8 @@ function scopeFlags(paths, options = []) {
1388
2482
  }
1389
2483
 
1390
2484
  // src/roles/format/deliberate-import-order.ts
1391
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "node:fs";
1392
- import { join as join8 } from "node:path";
2485
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "node:fs";
2486
+ import { join as join12 } from "node:path";
1393
2487
  import { parseSync } from "oxc-parser";
1394
2488
  import { format } from "oxfmt";
1395
2489
 
@@ -1526,8 +2620,8 @@ async function pinDeliberateImportOrder(cwd, options) {
1526
2620
  if (!options.sortImports) return [];
1527
2621
  const pinned = [];
1528
2622
  for (const file of listSourceFiles(cwd, CODE_EXTENSIONS).sort()) {
1529
- const path2 = join8(cwd, file);
1530
- const text = readFileSync6(path2, "utf8");
2623
+ const path2 = join12(cwd, file);
2624
+ const text = readFileSync7(path2, "utf8");
1531
2625
  if (!text.includes("eslint-disable") && !MODULE_NAMES.some((name) => text.includes(name)))
1532
2626
  continue;
1533
2627
  const before = readImports(file, text);
@@ -1535,7 +2629,7 @@ async function pinDeliberateImportOrder(cwd, options) {
1535
2629
  const sorted = await format(file, text, options);
1536
2630
  if (sorted.errors.length > 0) continue;
1537
2631
  if (!wouldMove(before, readImports(file, sorted.code))) continue;
1538
- writeFileSync2(path2, pin(text, before));
2632
+ writeFileSync3(path2, pin(text, before));
1539
2633
  pinned.push(file);
1540
2634
  }
1541
2635
  return pinned;
@@ -1569,8 +2663,8 @@ function writesWhenRewritten(name, command) {
1569
2663
  var CHECK_SCRIPT_NAMES = /* @__PURE__ */ new Set(["lint", "format", "check", "test", "typecheck", "verify"]);
1570
2664
 
1571
2665
  // src/roles/format/inherited-ignores.ts
1572
- import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
1573
- import { join as join9 } from "node:path";
2666
+ import { existsSync as existsSync8, readFileSync as readFileSync8 } from "node:fs";
2667
+ import { join as join13 } from "node:path";
1574
2668
  var ROOT_IGNORE_FILE = ".prettierignore";
1575
2669
  function isPattern(line) {
1576
2670
  const trimmed = line.trim();
@@ -1586,11 +2680,11 @@ function toModulePattern(pattern) {
1586
2680
  }
1587
2681
  function inheritedIgnorePatterns(workspaceRoot) {
1588
2682
  if (!workspaceRoot) return [];
1589
- const path2 = join9(workspaceRoot, ROOT_IGNORE_FILE);
1590
- if (!existsSync6(path2)) return [];
2683
+ const path2 = join13(workspaceRoot, ROOT_IGNORE_FILE);
2684
+ if (!existsSync8(path2)) return [];
1591
2685
  let contents;
1592
2686
  try {
1593
- contents = readFileSync7(path2, "utf8");
2687
+ contents = readFileSync8(path2, "utf8");
1594
2688
  } catch {
1595
2689
  return [];
1596
2690
  }
@@ -1635,8 +2729,8 @@ function needsSvelteCompiler(output) {
1635
2729
  }
1636
2730
 
1637
2731
  // src/roles/format/prettier-config.ts
1638
- import { existsSync as existsSync7, readFileSync as readFileSync8 } from "node:fs";
1639
- import { join as join10 } from "node:path";
2732
+ import { existsSync as existsSync9, readFileSync as readFileSync9 } from "node:fs";
2733
+ import { join as join14 } from "node:path";
1640
2734
  var UNSUPPORTED = /* @__PURE__ */ new Set(["plugins", "parser", "filepath", "rangeStart", "rangeEnd"]);
1641
2735
  function toOxfmtOverrides(value) {
1642
2736
  if (!Array.isArray(value)) return { overrides: [], unresolved: [] };
@@ -1661,14 +2755,14 @@ function toOxfmtOverrides(value) {
1661
2755
  return { overrides, unresolved };
1662
2756
  }
1663
2757
  function readPrettierSettings(cwd) {
1664
- const file = PRETTIER_CONFIG_FILES.find((name) => existsSync7(join10(cwd, name)));
2758
+ const file = PRETTIER_CONFIG_FILES.find((name) => existsSync9(join14(cwd, name)));
1665
2759
  if (!file) return { options: {}, unresolved: [] };
1666
2760
  if (/\.(js|cjs|mjs)$/.test(file)) {
1667
2761
  return { options: {}, file, unresolved: [`${file} is JavaScript, so it was not executed`] };
1668
2762
  }
1669
2763
  let parsed;
1670
2764
  try {
1671
- parsed = parseJsonc(readFileSync8(join10(cwd, file), "utf8"), file);
2765
+ parsed = parseJsonc(readFileSync9(join14(cwd, file), "utf8"), file);
1672
2766
  } catch {
1673
2767
  return { options: {}, file, unresolved: [`${file} could not be parsed`] };
1674
2768
  }
@@ -1786,7 +2880,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1786
2880
  manifestOperation(context2.cwd, this.formatScripts(context2.cwd))
1787
2881
  ];
1788
2882
  const prettierConfigs = PRETTIER_CONFIG_FILES.filter(
1789
- (name) => existsSync8(join11(context2.cwd, name))
2883
+ (name) => existsSync10(join15(context2.cwd, name))
1790
2884
  );
1791
2885
  for (const name of prettierConfigs) operations.push({ kind: "delete", path: name });
1792
2886
  const removableDeps = this.modulePrettierDependencies(context2.cwd);
@@ -1979,7 +3073,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1979
3073
  const adoption = readFormatAdoption(ctx.cwd);
1980
3074
  const preset = formatPresetFor(adoption.preset ?? DEFAULT_PRESET);
1981
3075
  const options = { ...preset, ...adoption.local };
1982
- const describe = (value) => typeof value === "string" ? value : JSON.stringify(value);
3076
+ const describe2 = (value) => typeof value === "string" ? value : JSON.stringify(value);
1983
3077
  return {
1984
3078
  runner: "oxfmt",
1985
3079
  configFile: adoption.configFile,
@@ -1988,7 +3082,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1988
3082
  options,
1989
3083
  overrides: Object.entries(adoption.local).filter(([key]) => !ALWAYS_LOCAL_KEYS.includes(key)).map(([key, value]) => ({
1990
3084
  rule: key,
1991
- reason: key in preset ? `${describe(value)}, where the ${adoption.preset ?? DEFAULT_PRESET} preset says ${describe(preset[key])}` : `${describe(value)}, which the preset does not set`
3085
+ reason: key in preset ? `${describe2(value)}, where the ${adoption.preset ?? DEFAULT_PRESET} preset says ${describe2(preset[key])}` : `${describe2(value)}, which the preset does not set`
1992
3086
  })),
1993
3087
  // Which files this module does not format at all. A long list here is worth a look:
1994
3088
  // it is the quiet way a module stops being covered.
@@ -2063,7 +3157,7 @@ var OxfmtAdapter = class extends BaseAdapter {
2063
3157
  modulePrettierDependencies(cwd) {
2064
3158
  const isPrettierPackage = (name) => name === "prettier" || name.startsWith("prettier-plugin-") || name.startsWith("@prettier/");
2065
3159
  try {
2066
- const manifest = JSON.parse(readFileSync9(join11(cwd, "package.json"), "utf8"));
3160
+ const manifest = JSON.parse(readFileSync10(join15(cwd, "package.json"), "utf8"));
2067
3161
  return Object.keys(manifest.devDependencies ?? {}).filter(isPrettierPackage).map((name) => ["devDependencies", name]);
2068
3162
  } catch {
2069
3163
  return [];
@@ -2145,8 +3239,8 @@ function registerFormat() {
2145
3239
 
2146
3240
  // src/roles/lint/adapters/oxlint/oxlint.adapter.ts
2147
3241
  import { spawnSync as spawnSync4 } from "node:child_process";
2148
- import { existsSync as existsSync12, readFileSync as readFileSync14, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
2149
- import { join as join17 } from "node:path";
3242
+ import { existsSync as existsSync14, readFileSync as readFileSync15, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "node:fs";
3243
+ import { join as join21 } from "node:path";
2150
3244
 
2151
3245
  // src/core/config/deferred-rules.ts
2152
3246
  function deferredRuleNames(rules) {
@@ -2154,8 +3248,8 @@ function deferredRuleNames(rules) {
2154
3248
  }
2155
3249
 
2156
3250
  // src/core/fix-report.ts
2157
- import { statSync as statSync2 } from "node:fs";
2158
- import { join as join12 } from "node:path";
3251
+ import { statSync as statSync3 } from "node:fs";
3252
+ import { join as join16 } from "node:path";
2159
3253
  var CONFIG_REFUSED = /failed to parse .*config|invalid config file/i;
2160
3254
  function readFailure(output) {
2161
3255
  const lines = output.split("\n").map((line2) => line2.trim());
@@ -2166,7 +3260,7 @@ function readFailure(output) {
2166
3260
  }
2167
3261
  function fingerprint(path2) {
2168
3262
  try {
2169
- const stat = statSync2(path2);
3263
+ const stat = statSync3(path2);
2170
3264
  return `${stat.size}:${stat.mtimeMs}`;
2171
3265
  } catch {
2172
3266
  return void 0;
@@ -2175,7 +3269,7 @@ function fingerprint(path2) {
2175
3269
  function snapshotFiles(cwd, extensions) {
2176
3270
  const snapshot = /* @__PURE__ */ new Map();
2177
3271
  for (const file of listSourceFiles(cwd, extensions)) {
2178
- const mark = fingerprint(join12(cwd, file));
3272
+ const mark = fingerprint(join16(cwd, file));
2179
3273
  if (mark !== void 0) snapshot.set(file, mark);
2180
3274
  }
2181
3275
  return snapshot;
@@ -2183,7 +3277,7 @@ function snapshotFiles(cwd, extensions) {
2183
3277
  function changedSince(cwd, before, extensions) {
2184
3278
  const changed = [];
2185
3279
  for (const file of listSourceFiles(cwd, extensions)) {
2186
- const now = fingerprint(join12(cwd, file));
3280
+ const now = fingerprint(join16(cwd, file));
2187
3281
  if (now === void 0) continue;
2188
3282
  if (before.get(file) !== now) changed.push(file);
2189
3283
  }
@@ -4153,7 +5247,7 @@ var react_default = {
4153
5247
  };
4154
5248
 
4155
5249
  // src/roles/lint/presets/shared.json
4156
- var shared_default = {
5250
+ var shared_default2 = {
4157
5251
  rules: {
4158
5252
  "no-var": "error",
4159
5253
  "prefer-const": [
@@ -4530,7 +5624,7 @@ var tools_default = {
4530
5624
  };
4531
5625
 
4532
5626
  // src/roles/lint/preset-data.ts
4533
- var SHARED_RULES = shared_default.rules;
5627
+ var SHARED_RULES = shared_default2.rules;
4534
5628
  var PRESET_FILES = {
4535
5629
  nest: nest_default,
4536
5630
  node: node_default,
@@ -4643,14 +5737,14 @@ function downgradedRulesFor(preset) {
4643
5737
  }
4644
5738
 
4645
5739
  // src/roles/lint/extra-layers.ts
4646
- import { existsSync as existsSync9, readFileSync as readFileSync10 } from "node:fs";
4647
- import { isAbsolute, join as join13, resolve as resolve2 } from "node:path";
5740
+ import { existsSync as existsSync11, readFileSync as readFileSync11 } from "node:fs";
5741
+ import { isAbsolute, join as join17, resolve as resolve2 } from "node:path";
4648
5742
  function ruleCount(cwd, specifier) {
4649
5743
  const path2 = isAbsolute(specifier) ? specifier : resolve2(cwd, specifier);
4650
- if (!existsSync9(path2)) return void 0;
5744
+ if (!existsSync11(path2)) return void 0;
4651
5745
  try {
4652
5746
  const parsed = parseJsonc(
4653
- readFileSync10(path2, "utf8"),
5747
+ readFileSync11(path2, "utf8"),
4654
5748
  specifier
4655
5749
  );
4656
5750
  return Object.keys(parsed.rules ?? {}).length;
@@ -4670,7 +5764,7 @@ function extraLayers(cwd, extendsList) {
4670
5764
  function committedExtendsList(cwd, configFile) {
4671
5765
  try {
4672
5766
  const parsed = parseJsonc(
4673
- readFileSync10(join13(cwd, configFile), "utf8"),
5767
+ readFileSync11(join17(cwd, configFile), "utf8"),
4674
5768
  configFile
4675
5769
  );
4676
5770
  return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
@@ -4853,12 +5947,12 @@ function lintPresetFor(preset) {
4853
5947
  }
4854
5948
 
4855
5949
  // src/roles/lint/adapters/oxlint/plan.ts
4856
- import { existsSync as existsSync11, readFileSync as readFileSync13 } from "node:fs";
4857
- import { join as join16 } from "node:path";
5950
+ import { existsSync as existsSync13, readFileSync as readFileSync14 } from "node:fs";
5951
+ import { join as join20 } from "node:path";
4858
5952
 
4859
5953
  // src/roles/lint/eslint-ignores.ts
4860
- import { existsSync as existsSync10, readFileSync as readFileSync11 } from "node:fs";
4861
- import { join as join14 } from "node:path";
5954
+ import { existsSync as existsSync12, readFileSync as readFileSync12 } from "node:fs";
5955
+ import { join as join18 } from "node:path";
4862
5956
  import { parseSync as parseSync2 } from "oxc-parser";
4863
5957
  var OPAQUE_SOURCE = /\b(includeIgnoreFile)\s*\([^)]*\)/g;
4864
5958
  function toOxlintPattern(pattern) {
@@ -4870,11 +5964,11 @@ function readRootEslintIgnores(root) {
4870
5964
  return { patterns: patterns.filter((pattern) => pattern.startsWith("**/")), unresolved };
4871
5965
  }
4872
5966
  function readEslintIgnores(cwd) {
4873
- const config = ESLINT_CONFIG_FILES.map((name) => join14(cwd, name)).find((path2) => existsSync10(path2));
5967
+ const config = ESLINT_CONFIG_FILES.map((name) => join18(cwd, name)).find((path2) => existsSync12(path2));
4874
5968
  if (!config) return { patterns: [], unresolved: [] };
4875
5969
  let source;
4876
5970
  try {
4877
- source = readFileSync11(config, "utf8");
5971
+ source = readFileSync12(config, "utf8");
4878
5972
  } catch {
4879
5973
  return { patterns: [], unresolved: [] };
4880
5974
  }
@@ -4945,8 +6039,8 @@ function keepsOtherCommands3(script) {
4945
6039
  }
4946
6040
 
4947
6041
  // src/roles/lint/rename-suppressions.ts
4948
- import { readdirSync as readdirSync2, readFileSync as readFileSync12, statSync as statSync3 } from "node:fs";
4949
- import { join as join15, relative as relative3 } from "node:path";
6042
+ import { readdirSync as readdirSync2, readFileSync as readFileSync13, statSync as statSync4 } from "node:fs";
6043
+ import { join as join19, relative as relative3 } from "node:path";
4950
6044
  var SOURCE_EXTENSIONS = [
4951
6045
  ".ts",
4952
6046
  ".tsx",
@@ -4993,10 +6087,10 @@ function* sourceFiles(dir) {
4993
6087
  return;
4994
6088
  }
4995
6089
  for (const entry of entries) {
4996
- const full = join15(dir, entry);
6090
+ const full = join19(dir, entry);
4997
6091
  let isDirectory;
4998
6092
  try {
4999
- isDirectory = statSync3(full).isDirectory();
6093
+ isDirectory = statSync4(full).isDirectory();
5000
6094
  } catch {
5001
6095
  continue;
5002
6096
  }
@@ -5013,7 +6107,7 @@ function findSuppressionRenames(cwd, renames) {
5013
6107
  for (const file of sourceFiles(cwd)) {
5014
6108
  let content;
5015
6109
  try {
5016
- content = readFileSync12(file, "utf8");
6110
+ content = readFileSync13(file, "utf8");
5017
6111
  } catch {
5018
6112
  continue;
5019
6113
  }
@@ -5159,7 +6253,7 @@ function plan2(context2) {
5159
6253
  keys: removableDeps
5160
6254
  });
5161
6255
  }
5162
- const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync11(join16(context2.cwd, name)));
6256
+ const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync13(join20(context2.cwd, name)));
5163
6257
  for (const name of eslintConfigs) operations.push({ kind: "delete", path: name });
5164
6258
  operations.push(
5165
6259
  ...nxTargetOperations({
@@ -5273,7 +6367,7 @@ function lintScripts(cwd) {
5273
6367
  function committedExtends(cwd) {
5274
6368
  try {
5275
6369
  const parsed = parseJsonc(
5276
- readFileSync13(join16(cwd, LINT_CONFIG_FILE), "utf8"),
6370
+ readFileSync14(join20(cwd, LINT_CONFIG_FILE), "utf8"),
5277
6371
  LINT_CONFIG_FILE
5278
6372
  );
5279
6373
  return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
@@ -5285,7 +6379,7 @@ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array
5285
6379
  function committedConfig(cwd) {
5286
6380
  try {
5287
6381
  const parsed = parseJsonc(
5288
- readFileSync13(join16(cwd, LINT_CONFIG_FILE), "utf8"),
6382
+ readFileSync14(join20(cwd, LINT_CONFIG_FILE), "utf8"),
5289
6383
  LINT_CONFIG_FILE
5290
6384
  );
5291
6385
  return isRecord2(parsed) ? parsed : {};
@@ -5309,7 +6403,7 @@ function describeGoverned(rule, variant) {
5309
6403
  function committedIgnorePatterns(cwd) {
5310
6404
  try {
5311
6405
  const parsed = parseJsonc(
5312
- readFileSync13(join16(cwd, LINT_CONFIG_FILE), "utf8"),
6406
+ readFileSync14(join20(cwd, LINT_CONFIG_FILE), "utf8"),
5313
6407
  LINT_CONFIG_FILE
5314
6408
  );
5315
6409
  return Array.isArray(parsed.ignorePatterns) ? parsed.ignorePatterns.filter((entry) => typeof entry === "string") : [];
@@ -5321,7 +6415,7 @@ function moduleEslintDependencies(cwd) {
5321
6415
  const isEslintPackage = (name) => name === "eslint" || name === "@types/eslint" || name === "typescript-eslint" || name.startsWith("@typescript-eslint/") || name.startsWith("eslint-plugin-") || name.startsWith("eslint-config-") || name.startsWith("@eslint/");
5322
6416
  let manifest;
5323
6417
  try {
5324
- manifest = JSON.parse(readFileSync13(join16(cwd, "package.json"), "utf8"));
6418
+ manifest = JSON.parse(readFileSync14(join20(cwd, "package.json"), "utf8"));
5325
6419
  } catch {
5326
6420
  return [];
5327
6421
  }
@@ -5502,23 +6596,23 @@ ${result.stderr ?? ""}`);
5502
6596
  * build the developer can see, rather than a silent half-adoption they cannot.
5503
6597
  */
5504
6598
  writeModuleBaseline(ctx, oxlint, env) {
5505
- const configPath = join17(ctx.cwd, LINT_CONFIG_FILE);
5506
- const baselinePath = join17(ctx.cwd, LINT_BASELINE_FILE);
5507
- if (!existsSync12(configPath)) return;
6599
+ const configPath = join21(ctx.cwd, LINT_CONFIG_FILE);
6600
+ const baselinePath = join21(ctx.cwd, LINT_BASELINE_FILE);
6601
+ if (!existsSync14(configPath)) return;
5508
6602
  let config;
5509
6603
  try {
5510
6604
  config = parseJsonc(
5511
- readFileSync14(configPath, "utf8"),
6605
+ readFileSync15(configPath, "utf8"),
5512
6606
  LINT_CONFIG_FILE
5513
6607
  );
5514
6608
  } catch {
5515
6609
  return;
5516
6610
  }
5517
6611
  const current = Array.isArray(config.extends) ? config.extends : [];
5518
- const measurePath = join17(ctx.cwd, LINT_MEASURE_FILE);
6612
+ const measurePath = join21(ctx.cwd, LINT_MEASURE_FILE);
5519
6613
  let measured;
5520
6614
  try {
5521
- writeFileSync3(
6615
+ writeFileSync4(
5522
6616
  measurePath,
5523
6617
  `${JSON.stringify({ ...config, extends: extendsWithBaseline(current, false) }, null, 2)}
5524
6618
  `
@@ -5553,7 +6647,7 @@ ${result.stderr ?? ""}`);
5553
6647
  );
5554
6648
  const next = extendsWithBaseline(current, holds.length > 0);
5555
6649
  if (holds.length > 0) {
5556
- writeFileSync3(baselinePath, renderBaseline(holds, readOwnPackage().version));
6650
+ writeFileSync4(baselinePath, renderBaseline(holds, readOwnPackage().version));
5557
6651
  process.stderr.write(` ${palette(process.stderr).warn(describeHolds(holds))}
5558
6652
  `);
5559
6653
  } else {
@@ -5561,12 +6655,12 @@ ${result.stderr ?? ""}`);
5561
6655
  }
5562
6656
  if (next.length !== current.length || next.some((entry, at) => entry !== current[at])) {
5563
6657
  config.extends = next;
5564
- writeFileSync3(configPath, `${JSON.stringify(config, null, 2)}
6658
+ writeFileSync4(configPath, `${JSON.stringify(config, null, 2)}
5565
6659
  `);
5566
6660
  }
5567
6661
  }
5568
6662
  async run(ctx) {
5569
- if (!existsSync12(join17(ctx.cwd, LINT_CONFIG_FILE))) {
6663
+ if (!existsSync14(join21(ctx.cwd, LINT_CONFIG_FILE))) {
5570
6664
  process.stderr.write(
5571
6665
  `sentinel lint(oxlint): no ${LINT_CONFIG_FILE} in this module; run \`sentinel --init --lint\` to adopt.
5572
6666
  `
@@ -5715,7 +6809,7 @@ ${result.stderr ?? ""}`);
5715
6809
  let stub;
5716
6810
  try {
5717
6811
  stub = parseJsonc(
5718
- readFileSync14(join17(cwd, LINT_CONFIG_FILE), "utf8"),
6812
+ readFileSync15(join21(cwd, LINT_CONFIG_FILE), "utf8"),
5719
6813
  LINT_CONFIG_FILE
5720
6814
  );
5721
6815
  } catch {
@@ -5725,7 +6819,7 @@ ${result.stderr ?? ""}`);
5725
6819
  for (const entry of stub.extends ?? []) {
5726
6820
  try {
5727
6821
  const preset = parseJsonc(
5728
- readFileSync14(join17(cwd, entry), "utf8"),
6822
+ readFileSync15(join21(cwd, entry), "utf8"),
5729
6823
  entry
5730
6824
  );
5731
6825
  for (const rule of Object.keys(preset.rules ?? {})) names.add(canonicalRuleName(rule));
@@ -5795,14 +6889,14 @@ ${result.stderr ?? ""}`);
5795
6889
  let parsed;
5796
6890
  try {
5797
6891
  parsed = parseJsonc(
5798
- readFileSync14(join17(cwd, LINT_CONFIG_FILE), "utf8"),
6892
+ readFileSync15(join21(cwd, LINT_CONFIG_FILE), "utf8"),
5799
6893
  LINT_CONFIG_FILE
5800
6894
  );
5801
6895
  } catch {
5802
6896
  return void 0;
5803
6897
  }
5804
6898
  const targets2 = Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : typeof parsed.extends === "string" ? [parsed.extends] : [];
5805
- return targets2.find((target) => !existsSync12(join17(cwd, target)));
6899
+ return targets2.find((target) => !existsSync14(join21(cwd, target)));
5806
6900
  }
5807
6901
  /** Announce what is not enforced, so reduced coverage is never silent. */
5808
6902
  announceDisabled(preset) {
@@ -5834,8 +6928,8 @@ function registerLint() {
5834
6928
 
5835
6929
  // src/roles/static-analysis/adapters/jscpd/jscpd.adapter.ts
5836
6930
  import { spawnSync as spawnSync6 } from "node:child_process";
5837
- import { existsSync as existsSync18, mkdirSync as mkdirSync2, readFileSync as readFileSync20, unlinkSync, writeFileSync as writeFileSync7 } from "node:fs";
5838
- import { join as join27 } from "node:path";
6931
+ import { existsSync as existsSync20, mkdirSync as mkdirSync3, readFileSync as readFileSync21, unlinkSync as unlinkSync2, writeFileSync as writeFileSync8 } from "node:fs";
6932
+ import { join as join31 } from "node:path";
5839
6933
  import { globSync as globSync5 } from "tinyglobby";
5840
6934
 
5841
6935
  // src/roles/static-analysis/bundle/budgets.ts
@@ -5864,32 +6958,32 @@ function bandFor(valueKb, referenceKb) {
5864
6958
  }
5865
6959
 
5866
6960
  // src/roles/static-analysis/bundle/scan.ts
5867
- import { existsSync as existsSync14, readFileSync as readFileSync16 } from "node:fs";
5868
- import { basename, join as join19 } from "node:path";
5869
- import { pathToFileURL } from "node:url";
6961
+ import { existsSync as existsSync16, readFileSync as readFileSync17 } from "node:fs";
6962
+ import { basename, join as join23 } from "node:path";
6963
+ import { pathToFileURL as pathToFileURL2 } from "node:url";
5870
6964
  import { gzipSync } from "node:zlib";
5871
6965
  import { globSync as globSync2 } from "tinyglobby";
5872
6966
 
5873
6967
  // src/roles/static-analysis/bundle/find-build.ts
5874
- import { existsSync as existsSync13, readFileSync as readFileSync15, statSync as statSync4 } from "node:fs";
5875
- import { join as join18 } from "node:path";
6968
+ import { existsSync as existsSync15, readFileSync as readFileSync16, statSync as statSync5 } from "node:fs";
6969
+ import { join as join22 } from "node:path";
5876
6970
  import { globSync } from "tinyglobby";
5877
6971
  var MANIFEST_GLOB = "server/_tanstack-start-manifest*.mjs";
5878
6972
  function candidateDirs(cwd) {
5879
6973
  const declared = readProjectPackageJson(cwd).nx?.targets?.build?.["outputs"];
5880
- const fromDeclaration = Array.isArray(declared) ? declared.filter((token) => typeof token === "string").map((token) => join18(cwd, token.replace(/^\{projectRoot\}\//, ""))) : [];
5881
- return [...fromDeclaration, join18(cwd, ".output"), join18(cwd, "dist"), join18(cwd, "build")];
6974
+ const fromDeclaration = Array.isArray(declared) ? declared.filter((token) => typeof token === "string").map((token) => join22(cwd, token.replace(/^\{projectRoot\}\//, ""))) : [];
6975
+ return [...fromDeclaration, join22(cwd, ".output"), join22(cwd, "dist"), join22(cwd, "build")];
5882
6976
  }
5883
6977
  function findBuildOutput(cwd) {
5884
6978
  const lookedIn = [];
5885
6979
  for (const dir of candidateDirs(cwd)) {
5886
6980
  lookedIn.push(dir);
5887
- if (!existsSync13(dir)) continue;
6981
+ if (!existsSync15(dir)) continue;
5888
6982
  const found = globSync(MANIFEST_GLOB, { cwd: dir, absolute: true, dot: false });
5889
6983
  if (found.length === 0) continue;
5890
6984
  const manifestPath = found[0];
5891
- const publicDir = join18(dir, "public");
5892
- if (!existsSync13(publicDir)) continue;
6985
+ const publicDir = join22(dir, "public");
6986
+ if (!existsSync15(publicDir)) continue;
5893
6987
  return { manifestPath, publicDir, ...dateOf(dir, manifestPath) };
5894
6988
  }
5895
6989
  const pkg = readProjectPackageJson(cwd);
@@ -5911,10 +7005,10 @@ function findBuildOutput(cwd) {
5911
7005
  };
5912
7006
  }
5913
7007
  function dateOf(dir, manifestPath) {
5914
- const record = join18(dir, "nitro.json");
5915
- if (existsSync13(record)) {
7008
+ const record = join22(dir, "nitro.json");
7009
+ if (existsSync15(record)) {
5916
7010
  try {
5917
- const parsed = JSON.parse(readFileSync15(record, "utf8"));
7011
+ const parsed = JSON.parse(readFileSync16(record, "utf8"));
5918
7012
  if (typeof parsed.date === "string") {
5919
7013
  const date = new Date(parsed.date);
5920
7014
  if (!Number.isNaN(date.getTime())) return { builtAt: date, builtAtFrom: "build record" };
@@ -5922,7 +7016,7 @@ function dateOf(dir, manifestPath) {
5922
7016
  } catch {
5923
7017
  }
5924
7018
  }
5925
- return { builtAt: statSync4(manifestPath).mtime, builtAtFrom: "file timestamp" };
7019
+ return { builtAt: statSync5(manifestPath).mtime, builtAtFrom: "file timestamp" };
5926
7020
  }
5927
7021
  function isBuildOutput(found) {
5928
7022
  return "manifestPath" in found;
@@ -5940,10 +7034,10 @@ function kindOf(path2) {
5940
7034
  return "other";
5941
7035
  }
5942
7036
  function resolveAnnounced(publicDir, announced) {
5943
- const direct = join19(publicDir, announced);
5944
- if (existsSync14(direct)) return direct;
5945
- const stripped = join19(publicDir, announced.replace(/^\/[^/]+\//, "/"));
5946
- return existsSync14(stripped) ? stripped : void 0;
7037
+ const direct = join23(publicDir, announced);
7038
+ if (existsSync16(direct)) return direct;
7039
+ const stripped = join23(publicDir, announced.replace(/^\/[^/]+\//, "/"));
7040
+ return existsSync16(stripped) ? stripped : void 0;
5947
7041
  }
5948
7042
  function emptyWeight() {
5949
7043
  return { transfer: 0, parsed: 0, files: 0 };
@@ -5979,7 +7073,7 @@ async function scanBundle(cwd) {
5979
7073
  unresolved.push(announced);
5980
7074
  measured.set(announced, void 0);
5981
7075
  } else {
5982
- const buffer = readFileSync16(path2);
7076
+ const buffer = readFileSync17(path2);
5983
7077
  measured.set(announced, {
5984
7078
  parsed: buffer.length,
5985
7079
  transfer: gzipSync(buffer, { level: 9 }).length
@@ -6030,7 +7124,7 @@ async function scanBundle(cwd) {
6030
7124
  }
6031
7125
  async function loadManifest(path2) {
6032
7126
  try {
6033
- const loaded = await import(pathToFileURL(path2).href);
7127
+ const loaded = await import(pathToFileURL2(path2).href);
6034
7128
  if (typeof loaded.tsrStartManifest !== "function") return void 0;
6035
7129
  const produced = loaded.tsrStartManifest();
6036
7130
  return produced !== null && typeof produced === "object" ? produced : void 0;
@@ -6066,7 +7160,7 @@ function scanDynamicLayer(publicDir, references) {
6066
7160
  })) {
6067
7161
  const name = basename(path2);
6068
7162
  if (referenced.has(name)) continue;
6069
- const buffer = readFileSync16(path2);
7163
+ const buffer = readFileSync17(path2);
6070
7164
  const size = { parsed: buffer.length, transfer: gzipSync(buffer, { level: 9 }).length };
6071
7165
  add(all2, size);
6072
7166
  const group = groupOf(name);
@@ -6094,28 +7188,28 @@ function scanDynamicLayer(publicDir, references) {
6094
7188
  }
6095
7189
 
6096
7190
  // src/roles/static-analysis/sources.ts
6097
- import { readFileSync as readFileSync17 } from "node:fs";
6098
- import { join as join22 } from "node:path";
7191
+ import { readFileSync as readFileSync18 } from "node:fs";
7192
+ import { join as join26 } from "node:path";
6099
7193
  import { parseSync as parseSync3 } from "oxc-parser";
6100
7194
  import { globSync as globSync3 } from "tinyglobby";
6101
7195
 
6102
7196
  // src/roles/static-analysis/adapters/jscpd/tool-config.ts
6103
- import { existsSync as existsSync16, mkdtempSync, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
7197
+ import { existsSync as existsSync18, mkdtempSync, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "node:fs";
6104
7198
  import { tmpdir } from "node:os";
6105
- import { join as join21 } from "node:path";
7199
+ import { join as join25 } from "node:path";
6106
7200
 
6107
7201
  // src/roles/static-analysis/configs/resolve.ts
6108
- import { existsSync as existsSync15 } from "node:fs";
6109
- import { join as join20 } from "node:path";
6110
- import { pathToFileURL as pathToFileURL2 } from "node:url";
7202
+ import { existsSync as existsSync17 } from "node:fs";
7203
+ import { join as join24 } from "node:path";
7204
+ import { pathToFileURL as pathToFileURL3 } from "node:url";
6111
7205
  var STATIC_CONFIG_FILE = "static-analysis.config.mjs";
6112
7206
  async function loadStaticConfig(cwd) {
6113
- const path2 = join20(cwd, STATIC_CONFIG_FILE);
6114
- if (!existsSync15(path2)) {
7207
+ const path2 = join24(cwd, STATIC_CONFIG_FILE);
7208
+ if (!existsSync17(path2)) {
6115
7209
  return { config: defineStaticAnalysis(), from: null, unreadable: null };
6116
7210
  }
6117
7211
  try {
6118
- const loaded = await import(pathToFileURL2(path2).href);
7212
+ const loaded = await import(pathToFileURL3(path2).href);
6119
7213
  return { config: adopt(loaded.default), from: STATIC_CONFIG_FILE, unreadable: null };
6120
7214
  } catch (error) {
6121
7215
  return {
@@ -6157,7 +7251,7 @@ function exclusions(config, scope = "tests") {
6157
7251
  return [.../* @__PURE__ */ new Set([...config.ignore, ...OWN_ARTEFACTS, ...tests])];
6158
7252
  }
6159
7253
  function measuredRoots(cwd) {
6160
- return existsSync16(join21(cwd, "src")) ? ["src"] : ["."];
7254
+ return existsSync18(join25(cwd, "src")) ? ["src"] : ["."];
6161
7255
  }
6162
7256
  function fileSettings(config, scope, extraEntries = []) {
6163
7257
  return {
@@ -6187,9 +7281,9 @@ function fileSettings(config, scope, extraEntries = []) {
6187
7281
  };
6188
7282
  }
6189
7283
  function duplicationRun(config, paths, scope) {
6190
- const dir = mkdtempSync(join21(tmpdir(), "sentinel-static-"));
6191
- const configPath = join21(dir, "jscpd.json");
6192
- writeFileSync4(configPath, `${JSON.stringify(fileSettings(config, scope), null, 2)}
7284
+ const dir = mkdtempSync(join25(tmpdir(), "sentinel-static-"));
7285
+ const configPath = join25(dir, "jscpd.json");
7286
+ writeFileSync5(configPath, `${JSON.stringify(fileSettings(config, scope), null, 2)}
6193
7287
  `, "utf8");
6194
7288
  const args = [
6195
7289
  "--config",
@@ -6214,9 +7308,9 @@ function duplicationRun(config, paths, scope) {
6214
7308
  return { args, rawReportDir: dir, dispose: () => rmSync3(dir, { force: true, recursive: true }) };
6215
7309
  }
6216
7310
  function deadCodeRun(config, paths, extraEntries = []) {
6217
- const dir = mkdtempSync(join21(tmpdir(), "sentinel-static-"));
6218
- const configPath = join21(dir, "jscpd.json");
6219
- writeFileSync4(
7311
+ const dir = mkdtempSync(join25(tmpdir(), "sentinel-static-"));
7312
+ const configPath = join25(dir, "jscpd.json");
7313
+ writeFileSync5(
6220
7314
  configPath,
6221
7315
  `${JSON.stringify(fileSettings(config, "dead-code", extraEntries), null, 2)}
6222
7316
  `,
@@ -6267,7 +7361,7 @@ function readParsed(cwd, files, passes) {
6267
7361
  function parsedSource(cwd, path2) {
6268
7362
  let text;
6269
7363
  try {
6270
- text = readFileSync17(join22(cwd, path2), "utf8");
7364
+ text = readFileSync18(join26(cwd, path2), "utf8");
6271
7365
  } catch {
6272
7366
  return void 0;
6273
7367
  }
@@ -6361,7 +7455,7 @@ function nameOf(fn, parent) {
6361
7455
  if (key) return key;
6362
7456
  return "(anonymous)";
6363
7457
  }
6364
- function lineAt(source, offset) {
7458
+ function lineAt2(source, offset) {
6365
7459
  if (offset === void 0) return 1;
6366
7460
  let line = 1;
6367
7461
  for (let index = 0; index < offset && index < source.length; index += 1) {
@@ -6383,7 +7477,7 @@ function complexityOfProgram(program, source) {
6383
7477
  if (!FUNCTION_NODES.has(node.type ?? "")) return;
6384
7478
  found.push({
6385
7479
  name: nameOf(node, parents.get(node)),
6386
- line: lineAt(source, node.start),
7480
+ line: lineAt2(source, node.start),
6387
7481
  complexity: complexityOf(node)
6388
7482
  });
6389
7483
  },
@@ -6454,13 +7548,13 @@ function unreadableNote(coverage) {
6454
7548
  }
6455
7549
 
6456
7550
  // src/roles/static-analysis/excerpt.ts
6457
- import { readFileSync as readFileSync18 } from "node:fs";
6458
- import { join as join24 } from "node:path";
7551
+ import { readFileSync as readFileSync19 } from "node:fs";
7552
+ import { join as join28 } from "node:path";
6459
7553
 
6460
7554
  // src/roles/static-analysis/maintainability/size-rules.ts
6461
- import { mkdtempSync as mkdtempSync2, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "node:fs";
7555
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "node:fs";
6462
7556
  import { tmpdir as tmpdir2 } from "node:os";
6463
- import { join as join23 } from "node:path";
7557
+ import { join as join27 } from "node:path";
6464
7558
  var GAUGE_THRESHOLDS = {
6465
7559
  // McCabe 1976 says 10, our p90 says 11. The only indicator where two independent candidates agree
6466
7560
  // within one unit, and 94.8% of our functions already sit in the 1-10 band SEI calls low risk.
@@ -6489,7 +7583,7 @@ var GAUGE_THRESHOLDS = {
6489
7583
  "cognitive-complexity": 7
6490
7584
  };
6491
7585
  var COGNITIVE = "cognitive-complexity";
6492
- var cognitivePlugin = () => join23(ownPackageDir(), "plugins", "sonar-cognitive.js");
7586
+ var cognitivePlugin = () => join27(ownPackageDir(), "plugins", "sonar-cognitive.js");
6493
7587
  var CATEGORIES = [
6494
7588
  "correctness",
6495
7589
  "suspicious",
@@ -6521,9 +7615,9 @@ function sizeRulesConfig() {
6521
7615
  };
6522
7616
  }
6523
7617
  function sizeRulesRun(paths, ignore) {
6524
- const dir = mkdtempSync2(join23(tmpdir2(), "sentinel-size-"));
6525
- const configPath = join23(dir, "oxlintrc.json");
6526
- writeFileSync5(configPath, `${JSON.stringify(sizeRulesConfig(), null, 2)}
7618
+ const dir = mkdtempSync2(join27(tmpdir2(), "sentinel-size-"));
7619
+ const configPath = join27(dir, "oxlintrc.json");
7620
+ writeFileSync6(configPath, `${JSON.stringify(sizeRulesConfig(), null, 2)}
6527
7621
  `, "utf8");
6528
7622
  return {
6529
7623
  // `--format json` because the console shape is rendered by this role from the figures: a
@@ -7047,23 +8141,23 @@ function helpDetails(help, withoutReference) {
7047
8141
  <p class="source">Source: ${prose(help.source)}</p>
7048
8142
  </details>`;
7049
8143
  }
7050
- function secondSide(finding) {
7051
- if (!finding.other) return "";
8144
+ function secondSide(finding2) {
8145
+ if (!finding2.other) return "";
7052
8146
  const file = (at) => at.split(":")[0] ?? at;
7053
- const sameFile = file(finding.other) === file(finding.path);
8147
+ const sameFile = file(finding2.other) === file(finding2.path);
7054
8148
  const label = sameFile ? "and line" : "and";
7055
- const shownSide = sameFile ? finding.other.split(":")[1] ?? finding.other : finding.other;
8149
+ const shownSide = sameFile ? finding2.other.split(":")[1] ?? finding2.other : finding2.other;
7056
8150
  return ` <span class="unit">${label}</span> <code>${escape(shownSide)}</code>`;
7057
8151
  }
7058
8152
  var HTML_GROUP_LIMIT = 50;
7059
8153
  function listedByMetric(findings) {
7060
8154
  const groups = /* @__PURE__ */ new Map();
7061
- for (const finding of findings) {
7062
- const bucket = groups.get(finding.metric) ?? [];
7063
- bucket.push(finding);
7064
- groups.set(finding.metric, bucket);
8155
+ for (const finding2 of findings) {
8156
+ const bucket = groups.get(finding2.metric) ?? [];
8157
+ bucket.push(finding2);
8158
+ groups.set(finding2.metric, bucket);
7065
8159
  }
7066
- const weight = (finding) => finding.value ?? finding.lines ?? 0;
8160
+ const weight = (finding2) => finding2.value ?? finding2.lines ?? 0;
7067
8161
  return new Map(
7068
8162
  [...groups.entries()].map(([metric, all2]) => {
7069
8163
  const sorted = [...all2].sort((left, right) => weight(right) - weight(left));
@@ -7093,7 +8187,7 @@ function caveatsOf(report) {
7093
8187
  }
7094
8188
  return found;
7095
8189
  }
7096
- function renderConsole(report) {
8190
+ function renderConsole2(report) {
7097
8191
  const lines = [];
7098
8192
  const width = (pick) => Math.max(...report.rows.map((row) => pick(row).length));
7099
8193
  const label = (row) => helpFor(row.metric)?.label ?? row.metric;
@@ -7148,7 +8242,7 @@ function renderConsole(report) {
7148
8242
  `;
7149
8243
  }
7150
8244
  var JSON_SCHEMA_VERSION = 1;
7151
- function renderJson(report) {
8245
+ function renderJson2(report) {
7152
8246
  const rows = report.rows.map((row) => {
7153
8247
  const help = helpFor(row.metric);
7154
8248
  return { ...row, help: help ? { label: help.label, caveat: help.caveat } : null };
@@ -7165,11 +8259,11 @@ function renderJson(report) {
7165
8259
  return `${JSON.stringify(envelope, null, 2)}
7166
8260
  `;
7167
8261
  }
7168
- function renderMarkdown(report) {
8262
+ function renderMarkdown2(report) {
7169
8263
  const version = report.settings.find((setting) => setting.key === "sentinel")?.value;
7170
8264
  const places = /* @__PURE__ */ new Map();
7171
- for (const finding of report.findings) {
7172
- places.set(finding.metric, (places.get(finding.metric) ?? 0) + 1);
8265
+ for (const finding2 of report.findings) {
8266
+ places.set(finding2.metric, (places.get(finding2.metric) ?? 0) + 1);
7173
8267
  }
7174
8268
  const figureLine = (row) => {
7175
8269
  const help = helpFor(row.metric);
@@ -7223,64 +8317,64 @@ function ruleChip(metric) {
7223
8317
  const threshold = GAUGE_THRESHOLDS[metric];
7224
8318
  return threshold === void 0 ? "" : ` <span class="chip"><code>${escape(metric)}</code> &gt; ${threshold}</span>`;
7225
8319
  }
7226
- function measureOf(finding) {
7227
- const metric = finding.metric;
8320
+ function measureOf(finding2) {
8321
+ const metric = finding2.metric;
7228
8322
  const number = (value, unit = "") => `<span class="pmeas">${grouped(value)}${unit === "" ? "" : `<span class="unit">${escape(unit)}</span>`}</span>`;
7229
- if (finding.value !== void 0) {
7230
- if (GAUGE_THRESHOLDS[metric] !== void 0) return number(finding.value);
7231
- if (metric.startsWith("complexity")) return number(finding.value, "complexity");
7232
- if (finding.dependsOn !== void 0) return number(finding.value, "imported by");
7233
- if (metric === "cycles") return number(finding.value, "files in the cycle");
7234
- return number(finding.value);
8323
+ if (finding2.value !== void 0) {
8324
+ if (GAUGE_THRESHOLDS[metric] !== void 0) return number(finding2.value);
8325
+ if (metric.startsWith("complexity")) return number(finding2.value, "complexity");
8326
+ if (finding2.dependsOn !== void 0) return number(finding2.value, "imported by");
8327
+ if (metric === "cycles") return number(finding2.value, "files in the cycle");
8328
+ return number(finding2.value);
7235
8329
  }
7236
- return finding.lines === void 0 ? "" : number(finding.lines, "lines");
8330
+ return finding2.lines === void 0 ? "" : number(finding2.lines, "lines");
7237
8331
  }
7238
- function metaOf(finding) {
8332
+ function metaOf(finding2) {
7239
8333
  const pairs = [];
7240
8334
  const add2 = (label, value) => {
7241
8335
  pairs.push(`<span class="pair"><span class="k">${escape(label)}</span>${value}</span>`);
7242
8336
  };
7243
- if (finding.name) {
8337
+ if (finding2.name) {
7244
8338
  add2(
7245
- finding.metric.startsWith("complexity") ? "function" : "name",
7246
- `<code>${escape(finding.name)}</code>`
8339
+ finding2.metric.startsWith("complexity") ? "function" : "name",
8340
+ `<code>${escape(finding2.name)}</code>`
7247
8341
  );
7248
8342
  }
7249
- if (finding.dependsOn !== void 0) add2("depends on", String(finding.dependsOn));
7250
- if (finding.instability !== void 0) add2("instability", finding.instability.toFixed(2));
7251
- if (finding.centrality !== void 0) add2("centrality", finding.centrality.toFixed(4));
7252
- if (finding.lines !== void 0 && finding.value !== void 0)
7253
- add2("length", `${finding.lines} lines`);
7254
- if (finding.confidence !== void 0) add2("confidence", `${finding.confidence}%`);
7255
- if (finding.reason) add2("unsure", `<span class="reason">${escape(finding.reason)}</span>`);
8343
+ if (finding2.dependsOn !== void 0) add2("depends on", String(finding2.dependsOn));
8344
+ if (finding2.instability !== void 0) add2("instability", finding2.instability.toFixed(2));
8345
+ if (finding2.centrality !== void 0) add2("centrality", finding2.centrality.toFixed(4));
8346
+ if (finding2.lines !== void 0 && finding2.value !== void 0)
8347
+ add2("length", `${finding2.lines} lines`);
8348
+ if (finding2.confidence !== void 0) add2("confidence", `${finding2.confidence}%`);
8349
+ if (finding2.reason) add2("unsure", `<span class="reason">${escape(finding2.reason)}</span>`);
7256
8350
  return pairs.length === 0 ? "" : ` <div class="pmeta">${pairs.join("")}</div>
7257
8351
  `;
7258
8352
  }
7259
- function codeSummary(finding) {
7260
- const count = finding.excerpt === void 0 ? 0 : finding.excerpt.split("\n").length;
7261
- if (finding.metric === "clones" || finding.metric === "clonesTests") {
7262
- return `${finding.lines ?? count} duplicated lines`;
8353
+ function codeSummary(finding2) {
8354
+ const count = finding2.excerpt === void 0 ? 0 : finding2.excerpt.split("\n").length;
8355
+ if (finding2.metric === "clones" || finding2.metric === "clonesTests") {
8356
+ return `${finding2.lines ?? count} duplicated lines`;
7263
8357
  }
7264
- if (finding.excerptFirstLine === 1) return `the first ${count} lines`;
7265
- return `${count} lines from ${finding.excerptFirstLine ?? "?"}`;
8358
+ if (finding2.excerptFirstLine === 1) return `the first ${count} lines`;
8359
+ return `${count} lines from ${finding2.excerptFirstLine ?? "?"}`;
7266
8360
  }
7267
- function codeBlock(finding) {
7268
- if (finding.excerpt === void 0) return "";
7269
- const first = finding.excerptFirstLine;
7270
- const body = first === void 0 ? escape(finding.excerpt) : finding.excerpt.split("\n").map((line, offset) => `<span class="n">${first + offset}</span>${escape(line)}`).join("\n");
8361
+ function codeBlock(finding2) {
8362
+ if (finding2.excerpt === void 0) return "";
8363
+ const first = finding2.excerptFirstLine;
8364
+ const body = first === void 0 ? escape(finding2.excerpt) : finding2.excerpt.split("\n").map((line, offset) => `<span class="n">${first + offset}</span>${escape(line)}`).join("\n");
7271
8365
  return ` <details class="pcode">
7272
- <summary>${escape(codeSummary(finding))}</summary>
8366
+ <summary>${escape(codeSummary(finding2))}</summary>
7273
8367
  <pre class="code">${body}</pre>
7274
8368
  </details>
7275
8369
  `;
7276
8370
  }
7277
- function placeCard(finding) {
8371
+ function placeCard(finding2) {
7278
8372
  return ` <div class="pcard">
7279
8373
  <div class="phead">
7280
- <code class="path">${escape(finding.path)}</code>${secondSide(finding)}
7281
- <span class="pright">${measureOf(finding)}${ruleChip(finding.metric)}</span>
8374
+ <code class="path">${escape(finding2.path)}</code>${secondSide(finding2)}
8375
+ <span class="pright">${measureOf(finding2)}${ruleChip(finding2.metric)}</span>
7282
8376
  </div>
7283
- ${metaOf(finding)}${codeBlock(finding)} </div>`;
8377
+ ${metaOf(finding2)}${codeBlock(finding2)} </div>`;
7284
8378
  }
7285
8379
  function placesFor(group) {
7286
8380
  const total = group.listed.length + group.rest;
@@ -7288,7 +8382,7 @@ function placesFor(group) {
7288
8382
  return ` <details class="place">
7289
8383
  <summary>where to look <span class="count">${grouped(total)} ${total === 1 ? "place" : "places"}</span>${capped}</summary>
7290
8384
  <div class="cards">
7291
- ${group.listed.map((finding) => placeCard(finding)).join("\n")}
8385
+ ${group.listed.map((finding2) => placeCard(finding2)).join("\n")}
7292
8386
  </div>
7293
8387
  ${group.rest > 0 ? ` <p class="sub">The other ${group.rest} are not on this page. <code>${SCRIPTS.json}</code> writes all of them, from the same run as this one.</p>` : ""}
7294
8388
  </details>`;
@@ -7345,17 +8439,17 @@ var KIND_UNITS = {
7345
8439
  cycles: "files",
7346
8440
  mostImported: "imported by"
7347
8441
  };
7348
- function findingRow(finding) {
7349
- const path2 = finding.path;
8442
+ function findingRow(finding2) {
8443
+ const path2 = finding2.path;
7350
8444
  const cut = path2.lastIndexOf("/");
7351
8445
  const dir = cut === -1 ? "" : path2.slice(0, cut + 1);
7352
8446
  const file = cut === -1 ? path2 : path2.slice(cut + 1);
7353
- const measure = finding.value ?? finding.lines;
7354
- const unit = KIND_UNITS[finding.metric];
7355
- const haystack = [path2, finding.name, finding.other].filter(Boolean).join(" ").toLowerCase();
7356
- return ` <tr data-kind="${escape(kindOf2(finding.metric))}" data-find="${escape(haystack)}">
7357
- <td><span class="kind">${escape(kindOf2(finding.metric))}</span></td>
7358
- <td class="where"><span class="dir">${escape(dir)}</span><b>${escape(file)}</b>${finding.name === void 0 ? "" : `<span class="sym">${escape(finding.name)}</span>`}${finding.other === void 0 ? "" : `<span class="sym">&#8596; ${escape(finding.other)}</span>`}</td>
8447
+ const measure = finding2.value ?? finding2.lines;
8448
+ const unit = KIND_UNITS[finding2.metric];
8449
+ const haystack = [path2, finding2.name, finding2.other].filter(Boolean).join(" ").toLowerCase();
8450
+ return ` <tr data-kind="${escape(kindOf2(finding2.metric))}" data-find="${escape(haystack)}">
8451
+ <td><span class="kind">${escape(kindOf2(finding2.metric))}</span></td>
8452
+ <td class="where"><span class="dir">${escape(dir)}</span><b>${escape(file)}</b>${finding2.name === void 0 ? "" : `<span class="sym">${escape(finding2.name)}</span>`}${finding2.other === void 0 ? "" : `<span class="sym">&#8596; ${escape(finding2.other)}</span>`}</td>
7359
8453
  <td class="meas">${measure === void 0 ? "" : `<b>${escape(typeof measure === "number" ? grouped(measure) : String(measure))}</b>${unit === void 0 ? "" : `<span>${escape(unit)}</span>`}`}</td>
7360
8454
  </tr>`;
7361
8455
  }
@@ -7365,7 +8459,7 @@ function findingsPanel(report) {
7365
8459
  return ` <p class="read">This run produced no finding to list.</p>`;
7366
8460
  }
7367
8461
  const counts = /* @__PURE__ */ new Map();
7368
- for (const finding of findings) counts.set(finding.metric, (counts.get(finding.metric) ?? 0) + 1);
8462
+ for (const finding2 of findings) counts.set(finding2.metric, (counts.get(finding2.metric) ?? 0) + 1);
7369
8463
  const chips = [...counts.entries()].map(
7370
8464
  ([metric, count]) => `<button type="button" class="fchip" data-kind="${escape(kindOf2(metric))}">${escape(kindOf2(metric))} <span>${grouped(count)}</span></button>`
7371
8465
  ).join("\n ");
@@ -7388,7 +8482,7 @@ function findingsPanel(report) {
7388
8482
  <tr><th scope="col">Kind</th><th scope="col">Where</th><th scope="col" class="meas">Measure</th></tr>
7389
8483
  </thead>
7390
8484
  <tbody>
7391
- ${findings.map((finding) => findingRow(finding)).join("\n")}
8485
+ ${findings.map((finding2) => findingRow(finding2)).join("\n")}
7392
8486
  </tbody>
7393
8487
  </table>`;
7394
8488
  }
@@ -7401,7 +8495,7 @@ function summaryParts(report, bold) {
7401
8495
  const withReference = report.rows.filter((row) => row.status !== void 0);
7402
8496
  const above = withReference.filter((row) => row.status === "exceed").length;
7403
8497
  const findings = report.findings ?? [];
7404
- const inTests = findings.filter((finding) => finding.metric === "clonesTests").length;
8498
+ const inTests = findings.filter((finding2) => finding2.metric === "clonesTests").length;
7405
8499
  const inProduct = findings.length - inTests;
7406
8500
  const thresholds = withReference.length === 0 ? [] : above === 0 ? [`${bold("No threshold exceeded")}: the measured levels are within the recommended norm`] : [`${bold(`${above} threshold${above === 1 ? "" : "s"} exceeded`)}`];
7407
8501
  return [
@@ -7980,7 +9074,7 @@ function linesOf(cache, cwd, relative5) {
7980
9074
  if (cache.has(relative5)) return cache.get(relative5);
7981
9075
  let lines;
7982
9076
  try {
7983
- lines = readFileSync18(join24(cwd, relative5), "utf8").split("\n");
9077
+ lines = readFileSync19(join28(cwd, relative5), "utf8").split("\n");
7984
9078
  } catch {
7985
9079
  lines = void 0;
7986
9080
  }
@@ -8003,16 +9097,16 @@ function declarationOf(lines, name) {
8003
9097
  }
8004
9098
  return hits.length === 1 ? hits[0] : void 0;
8005
9099
  }
8006
- function windowFor(finding, lines) {
8007
- const { line } = pointOf(finding.path);
9100
+ function windowFor(finding2, lines) {
9101
+ const { line } = pointOf(finding2.path);
8008
9102
  const head = () => ({
8009
9103
  firstLine: 1,
8010
9104
  text: lines.slice(0, WINDOW.lines).join("\n")
8011
9105
  });
8012
- if (WHOLE_FILE.has(finding.metric) || SHOWS_THE_HEAD.has(finding.metric)) return head();
8013
- const at = line ?? (finding.name ? declarationOf(lines, finding.name) : void 0);
9106
+ if (WHOLE_FILE.has(finding2.metric) || SHOWS_THE_HEAD.has(finding2.metric)) return head();
9107
+ const at = line ?? (finding2.name ? declarationOf(lines, finding2.name) : void 0);
8014
9108
  if (at === void 0) return void 0;
8015
- const nests = NESTS_UPWARD.has(finding.metric);
9109
+ const nests = NESTS_UPWARD.has(finding2.metric);
8016
9110
  const from = nests ? Math.max(1, at - WINDOW.nesting.before) : at;
8017
9111
  const to = Math.min(lines.length, nests ? at + WINDOW.nesting.after : at + WINDOW.lines - 1);
8018
9112
  if (from > lines.length) return void 0;
@@ -8021,18 +9115,18 @@ function windowFor(finding, lines) {
8021
9115
  function withExcerpts(report, cwd) {
8022
9116
  const wanted = /* @__PURE__ */ new Set();
8023
9117
  for (const group of listedByMetric(report.findings).values()) {
8024
- for (const finding of group.listed) {
8025
- if (finding.excerpt === void 0) wanted.add(finding);
9118
+ for (const finding2 of group.listed) {
9119
+ if (finding2.excerpt === void 0) wanted.add(finding2);
8026
9120
  }
8027
9121
  }
8028
9122
  if (wanted.size === 0) return report;
8029
9123
  const cache = /* @__PURE__ */ new Map();
8030
- const findings = report.findings.map((finding) => {
8031
- if (!wanted.has(finding)) return finding;
8032
- const lines = linesOf(cache, cwd, pointOf(finding.path).file);
8033
- if (lines === void 0) return finding;
8034
- const found = windowFor(finding, lines);
8035
- return found === void 0 ? finding : { ...finding, excerpt: found.text, excerptFirstLine: found.firstLine };
9124
+ const findings = report.findings.map((finding2) => {
9125
+ if (!wanted.has(finding2)) return finding2;
9126
+ const lines = linesOf(cache, cwd, pointOf(finding2.path).file);
9127
+ if (lines === void 0) return finding2;
9128
+ const found = windowFor(finding2, lines);
9129
+ return found === void 0 ? finding2 : { ...finding2, excerpt: found.text, excerptFirstLine: found.firstLine };
8036
9130
  });
8037
9131
  return { ...report, findings };
8038
9132
  }
@@ -8212,7 +9306,7 @@ var SCRIPT = `(function(){
8212
9306
  })();`;
8213
9307
 
8214
9308
  // src/roles/static-analysis/graph/imports.ts
8215
- import { dirname, join as join25 } from "node:path";
9309
+ import { dirname as dirname2, join as join29 } from "node:path";
8216
9310
  var TS_BEHIND_JS = {
8217
9311
  ".js": [".ts", ".tsx"],
8218
9312
  ".mjs": [".mts"],
@@ -8265,7 +9359,7 @@ function specifiersOf(program) {
8265
9359
  }
8266
9360
  function resolveSpecifier(from, specifier, known) {
8267
9361
  if (!specifier.startsWith(".")) return void 0;
8268
- const base = join25(dirname(from), specifier);
9362
+ const base = join29(dirname2(from), specifier);
8269
9363
  if (known.has(base)) return base;
8270
9364
  for (const extension of RESOLVABLE_EXTENSIONS) {
8271
9365
  if (known.has(base + extension)) return base + extension;
@@ -8276,7 +9370,7 @@ function resolveSpecifier(from, specifier, known) {
8276
9370
  if (known.has(swapped)) return swapped;
8277
9371
  }
8278
9372
  for (const extension of RESOLVABLE_EXTENSIONS) {
8279
- const index = join25(base, `index${extension}`);
9373
+ const index = join29(base, `index${extension}`);
8280
9374
  if (known.has(index)) return index;
8281
9375
  }
8282
9376
  return void 0;
@@ -8389,26 +9483,26 @@ function scanSyntax(cwd, config) {
8389
9483
 
8390
9484
  // src/roles/static-analysis/workspace/consumers.ts
8391
9485
  import { spawnSync as spawnSync5 } from "node:child_process";
8392
- import { existsSync as existsSync17, mkdirSync, readFileSync as readFileSync19, statSync as statSync5, writeFileSync as writeFileSync6 } from "node:fs";
8393
- import { dirname as dirname2, join as join26, relative as relative4, resolve as resolve3, sep } from "node:path";
8394
- var CACHE_DIR = join26("node_modules", ".cache", "sentinel");
9486
+ import { existsSync as existsSync19, mkdirSync as mkdirSync2, readFileSync as readFileSync20, statSync as statSync6, writeFileSync as writeFileSync7 } from "node:fs";
9487
+ import { dirname as dirname3, join as join30, relative as relative4, resolve as resolve3, sep } from "node:path";
9488
+ var CACHE_DIR = join30("node_modules", ".cache", "sentinel");
8395
9489
  var CACHE_FILE = "consumer-index.json";
8396
9490
  var SHAPE = 4;
8397
9491
  function workspaceRootFrom(cwd) {
8398
9492
  let dir = resolve3(cwd);
8399
9493
  for (; ; ) {
8400
- if (existsSync17(join26(dir, WORKSPACE_ROOT_MARKER))) return dir;
8401
- const parent = dirname2(dir);
9494
+ if (existsSync19(join30(dir, WORKSPACE_ROOT_MARKER))) return dir;
9495
+ const parent = dirname3(dir);
8402
9496
  if (parent === dir) return void 0;
8403
9497
  dir = parent;
8404
9498
  }
8405
9499
  }
8406
9500
  function aliasTable(workspaceRoot) {
8407
- const file = join26(workspaceRoot, "tsconfig.base.json");
8408
- if (!existsSync17(file)) return /* @__PURE__ */ new Map();
9501
+ const file = join30(workspaceRoot, "tsconfig.base.json");
9502
+ if (!existsSync19(file)) return /* @__PURE__ */ new Map();
8409
9503
  let paths;
8410
9504
  try {
8411
- const parsed = JSON.parse(readFileSync19(file, "utf8"));
9505
+ const parsed = JSON.parse(readFileSync20(file, "utf8"));
8412
9506
  paths = parsed.compilerOptions?.paths ?? {};
8413
9507
  } catch {
8414
9508
  return /* @__PURE__ */ new Map();
@@ -8426,7 +9520,7 @@ function aliasTable(workspaceRoot) {
8426
9520
  function resolveSpecifier2(specifier, table, workspaceRoot) {
8427
9521
  const isFile = (candidate) => {
8428
9522
  try {
8429
- return statSync5(join26(workspaceRoot, candidate)).isFile();
9523
+ return statSync6(join30(workspaceRoot, candidate)).isFile();
8430
9524
  } catch {
8431
9525
  return false;
8432
9526
  }
@@ -8463,11 +9557,11 @@ function resolveSpecifier2(specifier, table, workspaceRoot) {
8463
9557
  return void 0;
8464
9558
  }
8465
9559
  function resolveRelative(importer, specifier, workspaceRoot) {
8466
- const base = join26(dirname2(importer), specifier).split(sep).join("/");
9560
+ const base = join30(dirname3(importer), specifier).split(sep).join("/");
8467
9561
  if (base.startsWith("..")) return void 0;
8468
9562
  const isFile = (candidate) => {
8469
9563
  try {
8470
- return statSync5(join26(workspaceRoot, candidate)).isFile();
9564
+ return statSync6(join30(workspaceRoot, candidate)).isFile();
8471
9565
  } catch {
8472
9566
  return false;
8473
9567
  }
@@ -8565,10 +9659,10 @@ function hash(text) {
8565
9659
  }
8566
9660
  function consumerIndex(workspaceRoot, moduleRoots) {
8567
9661
  const key = cacheKey(workspaceRoot);
8568
- const file = join26(workspaceRoot, CACHE_DIR, CACHE_FILE);
8569
- if (key && existsSync17(file)) {
9662
+ const file = join30(workspaceRoot, CACHE_DIR, CACHE_FILE);
9663
+ if (key && existsSync19(file)) {
8570
9664
  try {
8571
- const cached = JSON.parse(readFileSync19(file, "utf8"));
9665
+ const cached = JSON.parse(readFileSync20(file, "utf8"));
8572
9666
  if (cached.key === key) {
8573
9667
  return {
8574
9668
  imported: new Set(cached.imported),
@@ -8582,14 +9676,14 @@ function consumerIndex(workspaceRoot, moduleRoots) {
8582
9676
  const index = buildConsumerIndex(workspaceRoot, moduleRoots());
8583
9677
  if (key) {
8584
9678
  try {
8585
- mkdirSync(join26(workspaceRoot, CACHE_DIR), { recursive: true });
9679
+ mkdirSync2(join30(workspaceRoot, CACHE_DIR), { recursive: true });
8586
9680
  const payload = {
8587
9681
  key,
8588
9682
  imported: [...index.imported],
8589
9683
  testOnly: [...index.testOnly],
8590
9684
  builtFrom: index.builtFrom
8591
9685
  };
8592
- writeFileSync6(file, JSON.stringify(payload), "utf8");
9686
+ writeFileSync7(file, JSON.stringify(payload), "utf8");
8593
9687
  } catch {
8594
9688
  }
8595
9689
  }
@@ -8686,7 +9780,7 @@ var JscpdAdapter = class extends BaseAdapter {
8686
9780
  operations: [
8687
9781
  // `write` and not `merge-json`: this file is code, and the engine's in-place merge is for
8688
9782
  // JSON. A re-init therefore RESTORES the stub, so an existing one is left alone below.
8689
- ...existsSync18(join27(context2.cwd, STATIC_CONFIG_FILE)) ? [] : [{ kind: "write", path: STATIC_CONFIG_FILE, contents: configStub() }],
9783
+ ...existsSync20(join31(context2.cwd, STATIC_CONFIG_FILE)) ? [] : [{ kind: "write", path: STATIC_CONFIG_FILE, contents: configStub() }],
8690
9784
  manifestOperation(context2.cwd, scripts()),
8691
9785
  ...nxTargetOperations({ cwd: context2.cwd, targets: targets() })
8692
9786
  // The report directory is ignored ONCE at the workspace root, by `--init`'s workspace prep,
@@ -8803,7 +9897,7 @@ var JscpdAdapter = class extends BaseAdapter {
8803
9897
  const written = [];
8804
9898
  for (const shape of shapes)
8805
9899
  written.push(...await emit(report, shape, ctx.cwd, graph, coverage));
8806
- process.stdout.write(renderConsole(report));
9900
+ process.stdout.write(renderConsole2(report));
8807
9901
  if (written.length > 0) process.stdout.write(`
8808
9902
  written: ${written.join(", ")}
8809
9903
  `);
@@ -8936,7 +10030,7 @@ var JscpdAdapter = class extends BaseAdapter {
8936
10030
  const declared = moduleScripts(ctx.cwd);
8937
10031
  const missing = Object.keys(scripts()).filter((name) => !(name in declared));
8938
10032
  const adopted = missing.length < Object.keys(scripts()).length;
8939
- const hasConfig = existsSync18(join27(ctx.cwd, STATIC_CONFIG_FILE));
10033
+ const hasConfig = existsSync20(join31(ctx.cwd, STATIC_CONFIG_FILE));
8940
10034
  return {
8941
10035
  adopted,
8942
10036
  preset: adopted ? ctx.preset : null,
@@ -9028,15 +10122,15 @@ function runDeadCode(jscpd, config, paths, cwd, extraEntries = []) {
9028
10122
  try {
9029
10123
  const ran = runTool(jscpd, args, cwd, "dead-code");
9030
10124
  if (ran.kind !== "ran") return ran;
9031
- const path2 = join27(rawReportDir, RAW_REPORT["dead-code"]);
9032
- if (!existsSync18(path2)) {
10125
+ const path2 = join31(rawReportDir, RAW_REPORT["dead-code"]);
10126
+ if (!existsSync20(path2)) {
9033
10127
  process.stderr.write(
9034
10128
  `sentinel static-analysis(jscpd): the dead-code pass wrote no ${RAW_REPORT["dead-code"]}
9035
10129
  `
9036
10130
  );
9037
10131
  return { kind: "failed", code: 1 };
9038
10132
  }
9039
- const parsed = safeParseDeadCode(readFileSync20(path2, "utf8"));
10133
+ const parsed = safeParseDeadCode(readFileSync21(path2, "utf8"));
9040
10134
  const statistics = parsed?.statistics;
9041
10135
  if (!statistics) {
9042
10136
  process.stderr.write(
@@ -9049,8 +10143,8 @@ function runDeadCode(jscpd, config, paths, cwd, extraEntries = []) {
9049
10143
  return { kind: "no-entry-point", files: statistics.files ?? 0 };
9050
10144
  }
9051
10145
  const byCategory = {};
9052
- for (const finding of parsed?.findings ?? []) {
9053
- const category = finding.category ?? "unknown";
10146
+ for (const finding2 of parsed?.findings ?? []) {
10147
+ const category = finding2.category ?? "unknown";
9054
10148
  byCategory[category] = (byCategory[category] ?? 0) + 1;
9055
10149
  }
9056
10150
  return { kind: "figures", statistics, byCategory, findings: parsed?.findings ?? [] };
@@ -9059,11 +10153,11 @@ function runDeadCode(jscpd, config, paths, cwd, extraEntries = []) {
9059
10153
  }
9060
10154
  }
9061
10155
  function moduleRelative(name, roots, cwd) {
9062
- if (existsSync18(join27(cwd, name))) return name;
10156
+ if (existsSync20(join31(cwd, name))) return name;
9063
10157
  for (const root of roots) {
9064
10158
  if (root === ".") continue;
9065
10159
  const candidate = `${root}/${name}`;
9066
- if (existsSync18(join27(cwd, candidate))) return candidate;
10160
+ if (existsSync20(join31(cwd, candidate))) return candidate;
9067
10161
  }
9068
10162
  return name;
9069
10163
  }
@@ -9072,8 +10166,8 @@ function runDuplication(jscpd, config, paths, scope, cwd) {
9072
10166
  try {
9073
10167
  const ran = runTool(jscpd, args, cwd, scope);
9074
10168
  if (ran.kind !== "ran") return ran;
9075
- const path2 = join27(rawReportDir, RAW_REPORT.duplication);
9076
- const parsed = existsSync18(path2) ? safeParse(readFileSync20(path2, "utf8")) : void 0;
10169
+ const path2 = join31(rawReportDir, RAW_REPORT.duplication);
10170
+ const parsed = existsSync20(path2) ? safeParse(readFileSync21(path2, "utf8")) : void 0;
9077
10171
  const total = parsed?.statistics?.total;
9078
10172
  if (!total) {
9079
10173
  process.stderr.write(
@@ -9137,7 +10231,7 @@ function placesToLook(product, tests, dead, complexity, graph, size, outsideTest
9137
10231
  const toolTestOnlyPaths = new Set(
9138
10232
  dead.kind === "figures" ? dead.findings.filter((f) => f.category === "unused-file" && f.reasons?.includes("used-only-by-tests")).map((f) => f.path ?? "").filter(Boolean) : []
9139
10233
  );
9140
- const metricOf = (finding) => finding.category === "unused-file" && finding.reasons?.includes("used-only-by-tests") === true ? "usedOnlyByTests" : deadMetric[finding.category ?? ""] ?? "deadLinesPercent";
10234
+ const metricOf = (finding2) => finding2.category === "unused-file" && finding2.reasons?.includes("used-only-by-tests") === true ? "usedOnlyByTests" : deadMetric[finding2.category ?? ""] ?? "deadLinesPercent";
9141
10235
  const weight = (f) => f.value ?? f.lines ?? 0;
9142
10236
  const inOrder = (all2) => [...all2].sort(
9143
10237
  (left, right) => left.metric.localeCompare(right.metric) || weight(right) - weight(left) || left.path.localeCompare(right.path) || (left.other ?? "").localeCompare(right.other ?? "")
@@ -9152,14 +10246,14 @@ function placesToLook(product, tests, dead, complexity, graph, size, outsideTest
9152
10246
  name: fn.name,
9153
10247
  value: fn.complexity
9154
10248
  })),
9155
- ...dead.kind === "figures" && !unreadable ? dead.findings.map((finding) => ({
9156
- metric: metricOf(finding),
9157
- path: finding.path ?? "(unknown)",
9158
- ...finding.name ? { name: finding.name } : {},
9159
- ...finding.lines === void 0 ? {} : { lines: finding.lines },
9160
- ...finding.confidence === void 0 ? {} : { confidence: finding.confidence },
9161
- ...finding.reasons?.length ? { reason: finding.reasons.join(", ") } : {},
9162
- ...finding.message ? { message: finding.message } : {}
10249
+ ...dead.kind === "figures" && !unreadable ? dead.findings.map((finding2) => ({
10250
+ metric: metricOf(finding2),
10251
+ path: finding2.path ?? "(unknown)",
10252
+ ...finding2.name ? { name: finding2.name } : {},
10253
+ ...finding2.lines === void 0 ? {} : { lines: finding2.lines },
10254
+ ...finding2.confidence === void 0 ? {} : { confidence: finding2.confidence },
10255
+ ...finding2.reasons?.length ? { reason: finding2.reasons.join(", ") } : {},
10256
+ ...finding2.message ? { message: finding2.message } : {}
9163
10257
  })) : [],
9164
10258
  /*
9165
10259
  * The files the workspace index found, which the tool cannot see.
@@ -9242,8 +10336,8 @@ function outsideOf(cwd) {
9242
10336
  function moduleRootsOf(workspaceRoot) {
9243
10337
  const roots = [];
9244
10338
  for (const area of ["apps", "libs"]) {
9245
- const base = join27(workspaceRoot, area);
9246
- if (!existsSync18(base)) continue;
10339
+ const base = join31(workspaceRoot, area);
10340
+ if (!existsSync20(base)) continue;
9247
10341
  for (const entry of globSync5([`${area}/**/package.json`, `${area}/**/project.json`], {
9248
10342
  cwd: workspaceRoot,
9249
10343
  ignore: ["**/node_modules/**"],
@@ -9400,8 +10494,8 @@ function assembleReport({
9400
10494
  }
9401
10495
  const toolTestOnly = new Set(
9402
10496
  dead.kind === "figures" ? dead.findings.filter(
9403
- (finding) => finding.category === "unused-file" && finding.reasons?.includes("used-only-by-tests") === true
9404
- ).map((finding) => finding.path ?? "").filter(Boolean) : []
10497
+ (finding2) => finding2.category === "unused-file" && finding2.reasons?.includes("used-only-by-tests") === true
10498
+ ).map((finding2) => finding2.path ?? "").filter(Boolean) : []
9405
10499
  );
9406
10500
  if (dead.kind === "figures" && !unreadable) {
9407
10501
  const stats = dead.statistics;
@@ -9576,23 +10670,23 @@ function assembleReport({
9576
10670
  byLanguage
9577
10671
  };
9578
10672
  }
9579
- var SHAPE_FILE = {
10673
+ var SHAPE_FILE2 = {
9580
10674
  json: `${TARGET}.json`,
9581
10675
  html: `${TARGET}.html`,
9582
10676
  markdown: `${TARGET}.md`
9583
10677
  };
9584
10678
  var PICTURE_FILE = `${TARGET}-graph.svg`;
9585
10679
  function clearStaleOutputs(cwd, keeping) {
9586
- for (const name of [...Object.values(SHAPE_FILE), PICTURE_FILE]) {
10680
+ for (const name of [...Object.values(SHAPE_FILE2), PICTURE_FILE]) {
9587
10681
  if (keeping.has(name)) continue;
9588
- const path2 = join27(cwd, REPORT_DIR, name);
9589
- if (existsSync18(path2)) unlinkSync(path2);
10682
+ const path2 = join31(cwd, REPORT_DIR, name);
10683
+ if (existsSync20(path2)) unlinkSync2(path2);
9590
10684
  }
9591
10685
  }
9592
10686
  function outputsOf(shapes, graph, coverage) {
9593
10687
  const keeping = /* @__PURE__ */ new Set();
9594
10688
  for (const shape of shapes) {
9595
- const name = SHAPE_FILE[shape];
10689
+ const name = SHAPE_FILE2[shape];
9596
10690
  if (name !== void 0) keeping.add(name);
9597
10691
  }
9598
10692
  if (shapes.includes("html") && drawsPicture(graph, coverage)) keeping.add(PICTURE_FILE);
@@ -9604,19 +10698,19 @@ function drawsPicture(graph, coverage) {
9604
10698
  async function emit(report, shape, cwd, graph, coverage) {
9605
10699
  if (shape === "console") return [];
9606
10700
  const detailed = shape === "markdown" ? report : withExcerpts(report, cwd);
9607
- mkdirSync2(join27(cwd, REPORT_DIR), { recursive: true });
10701
+ mkdirSync3(join31(cwd, REPORT_DIR), { recursive: true });
9608
10702
  const written = [];
9609
10703
  if (shape === "html" && drawsPicture(graph, coverage)) {
9610
10704
  const drawn = layOut(graph.graph, graph.communityOf);
9611
- const path2 = join27(cwd, REPORT_DIR, PICTURE_FILE);
10705
+ const path2 = join31(cwd, REPORT_DIR, PICTURE_FILE);
9612
10706
  await writeGraphPicture(drawn, path2);
9613
10707
  written.push(`${REPORT_DIR}/${PICTURE_FILE}`);
9614
10708
  detailed.picture = buildExplorer(drawn, graph.communityOf).markup;
9615
10709
  }
9616
- const RENDER = { json: renderJson, html: renderHtml, markdown: renderMarkdown };
9617
- const name = SHAPE_FILE[shape] ?? `${TARGET}.${shape}`;
10710
+ const RENDER = { json: renderJson2, html: renderHtml, markdown: renderMarkdown2 };
10711
+ const name = SHAPE_FILE2[shape] ?? `${TARGET}.${shape}`;
9618
10712
  const body = RENDER[shape](detailed);
9619
- writeFileSync7(join27(cwd, REPORT_DIR, name), body, "utf8");
10713
+ writeFileSync8(join31(cwd, REPORT_DIR, name), body, "utf8");
9620
10714
  written.unshift(`${REPORT_DIR}/${name}`);
9621
10715
  return written;
9622
10716
  }
@@ -9650,9 +10744,9 @@ function registerTest() {
9650
10744
 
9651
10745
  // src/roles/typescript/adapters/tsc/tsc.adapter.ts
9652
10746
  import { spawnSync as spawnSync7 } from "node:child_process";
9653
- import { existsSync as existsSync19, readFileSync as readFileSync22 } from "node:fs";
10747
+ import { existsSync as existsSync21, readFileSync as readFileSync23 } from "node:fs";
9654
10748
  import { createRequire } from "node:module";
9655
- import { join as join29 } from "node:path";
10749
+ import { join as join33 } from "node:path";
9656
10750
 
9657
10751
  // src/roles/typescript/presets/nest.json
9658
10752
  var nest_default2 = {
@@ -9704,7 +10798,7 @@ var react_default2 = {
9704
10798
  };
9705
10799
 
9706
10800
  // src/roles/typescript/presets/shared.json
9707
- var shared_default2 = {
10801
+ var shared_default3 = {
9708
10802
  $schema: "https://json.schemastore.org/tsconfig",
9709
10803
  compilerOptions: {
9710
10804
  strict: true,
@@ -9730,7 +10824,7 @@ var shared_default2 = {
9730
10824
  };
9731
10825
 
9732
10826
  // src/roles/typescript/preset-data.ts
9733
- var SHARED = shared_default2;
10827
+ var SHARED = shared_default3;
9734
10828
  var LAYERS = {
9735
10829
  nest: nest_default2,
9736
10830
  node: node_default2,
@@ -9837,8 +10931,8 @@ function readTsconfigAdoption(cwd) {
9837
10931
  }
9838
10932
 
9839
10933
  // src/roles/typescript/adapters/tsc/plan.ts
9840
- import { readFileSync as readFileSync21 } from "node:fs";
9841
- import { join as join28 } from "node:path";
10934
+ import { readFileSync as readFileSync22 } from "node:fs";
10935
+ import { join as join32 } from "node:path";
9842
10936
 
9843
10937
  // src/roles/typescript/typecheck-script.ts
9844
10938
  var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
@@ -9931,7 +11025,7 @@ function planAdoption(context2) {
9931
11025
  };
9932
11026
  }
9933
11027
  const existing = parseJsonc(
9934
- readFileSync21(join28(context2.cwd, target.path), "utf8"),
11028
+ readFileSync22(join32(context2.cwd, target.path), "utf8"),
9935
11029
  target.path
9936
11030
  );
9937
11031
  const extendsChain = composeExtends(existing.extends, preset);
@@ -10069,7 +11163,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
10069
11163
  let chain;
10070
11164
  try {
10071
11165
  const parsed = parseJsonc(
10072
- readFileSync22(join29(cwd, target.path), "utf8"),
11166
+ readFileSync23(join33(cwd, target.path), "utf8"),
10073
11167
  target.path
10074
11168
  );
10075
11169
  chain = parsed.extends;
@@ -10082,7 +11176,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
10082
11176
  );
10083
11177
  if (preset === void 0) return void 0;
10084
11178
  try {
10085
- createRequire(join29(cwd, "noop.js")).resolve(preset);
11179
+ createRequire(join33(cwd, "noop.js")).resolve(preset);
10086
11180
  return void 0;
10087
11181
  } catch {
10088
11182
  return preset;
@@ -10178,7 +11272,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
10178
11272
  referencedProjects(cwd, config) {
10179
11273
  try {
10180
11274
  const parsed = parseJsonc(
10181
- readFileSync22(join29(cwd, config), "utf8"),
11275
+ readFileSync23(join33(cwd, config), "utf8"),
10182
11276
  config
10183
11277
  );
10184
11278
  const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path2) => typeof path2 === "string" && path2.length > 0);
@@ -10194,7 +11288,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
10194
11288
  * check.
10195
11289
  */
10196
11290
  typecheckTarget(cwd) {
10197
- if (existsSync19(join29(cwd, "tsconfig.json"))) return "tsconfig.json";
11291
+ if (existsSync21(join33(cwd, "tsconfig.json"))) return "tsconfig.json";
10198
11292
  const target = resolveTsconfigTarget(cwd);
10199
11293
  return target.reason === "none" ? null : target.path;
10200
11294
  }
@@ -10319,6 +11413,7 @@ function registerAdapters() {
10319
11413
  registerBuild();
10320
11414
  registerTest();
10321
11415
  registerStaticAnalysis();
11416
+ registerEnv();
10322
11417
  }
10323
11418
 
10324
11419
  // src/core/detect-framework.ts
@@ -10365,12 +11460,12 @@ function diffLines(before, after) {
10365
11460
  { length: from.length + 1 },
10366
11461
  () => new Array(to.length + 1).fill(0)
10367
11462
  );
10368
- const cell = (i2, j2) => lcs[i2]?.[j2] ?? 0;
11463
+ const cell2 = (i2, j2) => lcs[i2]?.[j2] ?? 0;
10369
11464
  for (let i2 = from.length - 1; i2 >= 0; i2--) {
10370
11465
  const row = lcs[i2];
10371
11466
  if (!row) continue;
10372
11467
  for (let j2 = to.length - 1; j2 >= 0; j2--) {
10373
- row[j2] = from[i2] === to[j2] ? cell(i2 + 1, j2 + 1) + 1 : Math.max(cell(i2 + 1, j2), cell(i2, j2 + 1));
11468
+ row[j2] = from[i2] === to[j2] ? cell2(i2 + 1, j2 + 1) + 1 : Math.max(cell2(i2 + 1, j2), cell2(i2, j2 + 1));
10374
11469
  }
10375
11470
  }
10376
11471
  const out = [];
@@ -10381,7 +11476,7 @@ function diffLines(before, after) {
10381
11476
  out.push(` ${from[i] ?? ""}`);
10382
11477
  i++;
10383
11478
  j++;
10384
- } else if (cell(i + 1, j) >= cell(i, j + 1)) {
11479
+ } else if (cell2(i + 1, j) >= cell2(i, j + 1)) {
10385
11480
  out.push(`- ${from[i] ?? ""}`);
10386
11481
  i++;
10387
11482
  } else {
@@ -10400,7 +11495,7 @@ function replaceLines(current, replacements) {
10400
11495
  }
10401
11496
 
10402
11497
  // src/core/apply-plan.ts
10403
- import { existsSync as existsSync20, readFileSync as readFileSync23, renameSync, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "node:fs";
11498
+ import { existsSync as existsSync22, readFileSync as readFileSync24, renameSync, rmSync as rmSync5, writeFileSync as writeFileSync9 } from "node:fs";
10404
11499
  import { resolve as resolve4, sep as sep2 } from "node:path";
10405
11500
  import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
10406
11501
  function resolveWithinRoot(cwd, relativePath) {
@@ -10412,7 +11507,7 @@ function resolveWithinRoot(cwd, relativePath) {
10412
11507
  return absolutePath;
10413
11508
  }
10414
11509
  function readIfExists(absolutePath) {
10415
- return existsSync20(absolutePath) ? readFileSync23(absolutePath, "utf8") : void 0;
11510
+ return existsSync22(absolutePath) ? readFileSync24(absolutePath, "utf8") : void 0;
10416
11511
  }
10417
11512
  function* leaves(value, prefix = []) {
10418
11513
  for (const [key, keyValue] of Object.entries(value)) {
@@ -10514,7 +11609,7 @@ function preparePlan(cwd, plan3) {
10514
11609
  }
10515
11610
  function writeFileAtomic(absolutePath, contents) {
10516
11611
  const tempPath = `${absolutePath}.sentinel-${process.pid}.tmp`;
10517
- writeFileSync8(tempPath, contents);
11612
+ writeFileSync9(tempPath, contents);
10518
11613
  renameSync(tempPath, absolutePath);
10519
11614
  }
10520
11615
  function applyPlan(cwd, plan3) {
@@ -10530,8 +11625,8 @@ function applyPlan(cwd, plan3) {
10530
11625
  }
10531
11626
 
10532
11627
  // src/core/config/preset-evidence.ts
10533
- import { existsSync as existsSync21, readdirSync as readdirSync3, readFileSync as readFileSync24 } from "node:fs";
10534
- import { join as join30 } from "node:path";
11628
+ import { existsSync as existsSync23, readdirSync as readdirSync3, readFileSync as readFileSync25 } from "node:fs";
11629
+ import { join as join34 } from "node:path";
10535
11630
  var PATH_SIGNALS = [
10536
11631
  {
10537
11632
  preset: "nest",
@@ -10565,12 +11660,12 @@ function sourceImports(cwd, pattern) {
10565
11660
  }
10566
11661
  for (const entry of entries) {
10567
11662
  if (entry.isDirectory()) {
10568
- if (!NOT_SCANNED.has(entry.name)) stack.push(join30(dir, entry.name));
11663
+ if (!NOT_SCANNED.has(entry.name)) stack.push(join34(dir, entry.name));
10569
11664
  continue;
10570
11665
  }
10571
11666
  if (!SOURCE_FILE.test(entry.name)) continue;
10572
11667
  try {
10573
- if (pattern.test(readFileSync24(join30(dir, entry.name), "utf8"))) return true;
11668
+ if (pattern.test(readFileSync25(join34(dir, entry.name), "utf8"))) return true;
10574
11669
  } catch {
10575
11670
  continue;
10576
11671
  }
@@ -10579,11 +11674,11 @@ function sourceImports(cwd, pattern) {
10579
11674
  return false;
10580
11675
  }
10581
11676
  function dependencyNames(cwd) {
10582
- const path2 = join30(cwd, "package.json");
10583
- if (!existsSync21(path2)) return [];
11677
+ const path2 = join34(cwd, "package.json");
11678
+ if (!existsSync23(path2)) return [];
10584
11679
  try {
10585
11680
  const manifest = parseJsonc(
10586
- readFileSync24(path2, "utf8"),
11681
+ readFileSync25(path2, "utf8"),
10587
11682
  path2
10588
11683
  );
10589
11684
  return [
@@ -10606,7 +11701,7 @@ function reactTsconfigEvidence(cwd) {
10606
11701
  }
10607
11702
  for (const name of entries) {
10608
11703
  try {
10609
- const config = parseJsonc(readFileSync24(join30(cwd, name), "utf8"), name);
11704
+ const config = parseJsonc(readFileSync25(join34(cwd, name), "utf8"), name);
10610
11705
  if (config.compilerOptions?.jsx !== void 0) return 'its tsconfig sets "jsx"';
10611
11706
  const extended = Array.isArray(config.extends) ? config.extends : config.extends === void 0 ? [] : [config.extends];
10612
11707
  if (extended.some((entry) => entry === REACT_TYPESCRIPT_PRESET)) {
@@ -10813,6 +11908,7 @@ export {
10813
11908
  resolve,
10814
11909
  buildConfigFile,
10815
11910
  palette,
11911
+ ENV_SHAPES,
10816
11912
  presetNotInstalledYet,
10817
11913
  registerAdapters,
10818
11914
  correctingSignal,