@pho9ubenaa/siro 0.5.1 → 0.6.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.
@@ -1,16 +1,16 @@
1
1
  import path from "node:path";
2
- import { lstatSync, readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { readFileSync, readdirSync, statSync } from "node:fs";
3
3
  import { pathToFileURL } from "node:url";
4
4
  import satisfies from "semver/functions/satisfies.js";
5
5
  import * as vb from "valibot";
6
6
  import { lt, parse } from "semver";
7
7
  import valid from "semver/functions/valid.js";
8
- import { GLOBSTAR, Minimatch, braceExpand } from "minimatch";
8
+ import { Minimatch } from "minimatch";
9
9
  import ini from "ini";
10
10
  import { parse as parse$1 } from "smol-toml";
11
11
  import { parseDocument } from "yaml";
12
12
  import pc from "picocolors";
13
- //#region src/shared/errors.ts
13
+ //#region src/core/contracts/errors.ts
14
14
  var SiroError = class extends Error {
15
15
  exitCode;
16
16
  constructor(message, exitCode) {
@@ -47,7 +47,7 @@ const wrapCodecError = (filePath, fn) => {
47
47
  }
48
48
  };
49
49
  //#endregion
50
- //#region src/shared/paths.ts
50
+ //#region src/core/contracts/paths.ts
51
51
  const isRelPath = (value) => typeof value === "string" && value.length > 0 && !value.includes("\0") && !/^[\\/]/u.test(value) && !/^[a-z]:/iu.test(value) && !value.split(/[\\/]/u).includes("..");
52
52
  const asRelPath = (value) => {
53
53
  if (!isRelPath(value)) throw new TypeError("Expected a repository-relative path without parent traversal.");
@@ -62,8 +62,17 @@ const asAbsPath = (value) => {
62
62
  };
63
63
  const nodePaths = {
64
64
  isAbsolute: isAbsPath,
65
- resolve: (root, relative) => asAbsPath(path.join(root, asRelPath(relative))),
66
- normalizePattern: (pattern) => path.posix.normalize(pattern)
65
+ resolve(root, relative) {
66
+ if (typeof relative !== "string" || relative.includes("\0") || path.isAbsolute(relative) || relative.split(path.sep === "\\" ? /[\\/]/u : /\//u).includes("..")) throw new ConfigError(`Invalid repository-relative path: ${String(relative)}`);
67
+ const resolved = path.join(root, relative);
68
+ const within = path.relative(root, resolved);
69
+ if (within === ".." || within.startsWith(`..${path.sep}`) || path.isAbsolute(within)) throw new ConfigError("Path escapes repository root.");
70
+ return asAbsPath(resolved);
71
+ },
72
+ child(parent, name) {
73
+ if (typeof name !== "string" || !name || name === "." || name === ".." || name.includes("\0") || name.includes("/") || path.sep === "\\" && (name.includes("\\") || name.includes(":"))) throw new ConfigError(`FileSystem.readDirectories returned an invalid child directory name: ${String(name)}`);
74
+ return parent === "." ? name : `${parent}/${name}`;
75
+ }
67
76
  };
68
77
  //#endregion
69
78
  //#region src/adapters/node-errors.ts
@@ -78,24 +87,6 @@ const nodeFileSystem = {
78
87
  readDirectories(directory) {
79
88
  return readdirSync(directory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
80
89
  },
81
- resolveDirectory(parent, name) {
82
- let target;
83
- try {
84
- target = lstatSync(path.join(parent, name), { bigint: true });
85
- } catch (error) {
86
- if (isNodeError(error) && error.code === "ENOENT") return void 0;
87
- throw error;
88
- }
89
- if (!target.isDirectory()) return void 0;
90
- const names = readdirSync(parent, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
91
- if (names.includes(name)) return name;
92
- const matches = names.filter((candidate) => {
93
- const entry = lstatSync(path.join(parent, candidate), { bigint: true });
94
- return entry.isDirectory() && entry.dev === target.dev && entry.ino === target.ino;
95
- });
96
- if (target.ino === 0n || matches.length !== 1) throw new ConfigError(`${parent}: cannot identify the resolved workspace directory ${name}.`);
97
- return matches[0];
98
- },
99
90
  exists(filePath) {
100
91
  try {
101
92
  assertRegularFile(filePath);
@@ -121,7 +112,7 @@ const assertDirectory = (root) => {
121
112
  };
122
113
  //#endregion
123
114
  //#region package.json
124
- var version$1 = "0.5.1";
115
+ var version$1 = "0.6.0";
125
116
  var engines = { "node": "^22.18.0 || ^24.0.0" };
126
117
  //#endregion
127
118
  //#region src/version.ts
@@ -131,11 +122,20 @@ const SUPPORTED_NODE_RANGE = engines.node;
131
122
  //#region src/adapters/node-version.ts
132
123
  const isSupportedNodeVersion = (version) => satisfies(version, SUPPORTED_NODE_RANGE);
133
124
  //#endregion
134
- //#region src/domain/entities/pms.ts
125
+ //#region src/core/contracts/records.ts
126
+ /** Configuration maps are ordinary objects or null-prototype dictionaries. */
127
+ const isPlainRecord = (value) => {
128
+ if (typeof value !== "object" || value === null) return false;
129
+ const prototype = Object.getPrototypeOf(value);
130
+ return prototype === Object.prototype || prototype === null;
131
+ };
132
+ //#endregion
133
+ //#region src/core/contracts/pms.ts
135
134
  /**
136
135
  * Single source of truth for package managers and severities.
137
- * Types, runtime sets, and ordering are all derived from these tuples — add a
138
- * package manager here and the rest of the codebase follows.
136
+ * Types, runtime sets, and ordering derive from these tuples. Adding a manager
137
+ * still requires local detection signals, applicable rule bindings and verified
138
+ * policy/availability evidence; the tuple alone does not implement support.
139
139
  */
140
140
  const PMS = [
141
141
  "npm",
@@ -171,24 +171,68 @@ const parsePackageManagerField = (value) => {
171
171
  if (typeof name !== "undefined" && isPM(name)) return name;
172
172
  };
173
173
  //#endregion
174
- //#region src/domain/entities/project-type.ts
175
- const PROJECT_TYPES = ["application", "package"];
176
- const isProjectType = (value) => PROJECT_TYPES.some((projectType) => projectType === value);
174
+ //#region src/core/pm-versions.ts
175
+ /** Release history establishes stable versions, not ranges or prerelease builds. */
176
+ const isStableVersion = (value) => {
177
+ if (typeof value !== "string" || !/^\d/u.test(value) || value.trim() !== value) return false;
178
+ const version = parse(value);
179
+ return version !== null && version.prerelease.length === 0;
180
+ };
181
+ /** Keep name-only detection independent from whether the declaration pins a version. */
182
+ const declaredPMVersion = (declaration) => {
183
+ const [pm, version, ...extra] = declaration?.trim().split("@") ?? [];
184
+ return pm && isPM(pm) && isStableVersion(version) && extra.length === 0 ? { [pm]: version } : {};
185
+ };
177
186
  //#endregion
178
- //#region src/shared/records.ts
179
- /** Configuration maps are ordinary objects or null-prototype dictionaries. */
180
- const isPlainRecord = (value) => {
181
- if (typeof value !== "object" || value === null) return false;
182
- const prototype = Object.getPrototypeOf(value);
183
- return prototype === Object.prototype || prototype === null;
187
+ //#region src/core/inspection-options.ts
188
+ const denseArray = (value, name) => {
189
+ if (!Array.isArray(value)) throw new ConfigError(`${name} must be a dense array.`);
190
+ for (let index = 0; index < value.length; index += 1) if (!Object.hasOwn(value, index)) throw new ConfigError(`${name} must be a dense array.`);
191
+ return value;
192
+ };
193
+ const relativeInput = (value, name) => {
194
+ if (typeof value !== "string" || !value || value.includes("\0") || name === "exclude" && (value.includes("\\") || /^[a-z]:/iu.test(value)) || /^[\\/]/u.test(value) || /^[a-z]:[\\/]/iu.test(value) || value.split("/").includes("..")) throw new ConfigError(`${name}: expected a nonempty cwd-relative /-separated path without parent traversal.`);
195
+ return value.split("/").filter((part) => part && part !== ".").join("/") || ".";
196
+ };
197
+ const parseExcludes = (value) => [...new Set(denseArray(value, "exclude").map((input) => {
198
+ const pattern = relativeInput(input, "exclude");
199
+ if (pattern === "." || pattern.startsWith("!")) throw new ConfigError("exclude cannot exclude cwd (.) or re-include with leading !.");
200
+ return pattern;
201
+ }))];
202
+ const parseInstallationRoots = (value) => {
203
+ const roots = /* @__PURE__ */ new Map();
204
+ for (const input of denseArray(value, "installationRoots")) {
205
+ if (typeof input !== "string" && (!isPlainRecord(input) || Object.keys(input).some((key) => ![
206
+ "path",
207
+ "pm",
208
+ "pmVersion"
209
+ ].includes(key)))) throw new ConfigError("installationRoots entries must be paths or { path, pm?, pmVersion? }.");
210
+ const entry = typeof input === "string" ? { path: input } : input;
211
+ const path = relativeInput(entry.path, "installationRoots");
212
+ if (entry.pm !== void 0 && (typeof entry.pm !== "string" || !isPM(entry.pm)) || entry.pmVersion !== void 0 && (!entry.pm || typeof entry.pmVersion !== "string" || !isStableVersion(entry.pmVersion))) throw new ConfigError(`${path}: pmVersion requires pm and an exact stable version; pm must be a supported manager.`);
213
+ if (path === "." && (entry.pm !== void 0 || entry.pmVersion !== void 0)) throw new ConfigError("Use top-level pm / pmVersion / pms / pmVersions for installation root \".\".");
214
+ const root = {
215
+ path,
216
+ pm: entry.pm,
217
+ pmVersion: entry.pmVersion
218
+ };
219
+ const previous = roots.get(path);
220
+ if (previous && (previous.pm !== root.pm || previous.pmVersion !== root.pmVersion)) throw new ConfigError(`${path}: conflicting installationRoots entries.`);
221
+ roots.set(path, root);
222
+ }
223
+ return [...roots.values()];
184
224
  };
185
225
  //#endregion
186
- //#region src/domain/ports/reporter.ts
226
+ //#region src/core/contracts/project-type.ts
227
+ const PROJECT_TYPES = ["application", "package"];
228
+ const isProjectType = (value) => PROJECT_TYPES.some((projectType) => projectType === value);
229
+ //#endregion
230
+ //#region src/core/contracts/reporter.ts
187
231
  const isReporterShape = (value) => {
188
232
  return isPlainRecord(value) && typeof value.name === "string" && typeof value.format === "function";
189
233
  };
190
234
  //#endregion
191
- //#region src/domain/entities/config-value.ts
235
+ //#region src/core/contracts/config-value.ts
192
236
  const CODEC_KINDS = [
193
237
  "json",
194
238
  "npmrc",
@@ -211,14 +255,14 @@ const toParsedConfig = (value) => {
211
255
  throw new TypeError("Config root must be a mapping.");
212
256
  };
213
257
  //#endregion
214
- //#region src/domain/entities/config-file-ref.ts
258
+ //#region src/core/contracts/config-file-ref.ts
215
259
  const CONFIG_FILE_KINDS = new Set(CODEC_KINDS);
216
260
  const isConfigFileRefShape = (value) => {
217
261
  if (!isPlainRecord(value) || typeof value.kind !== "string" || !isRelPath(value.path)) return false;
218
262
  return CONFIG_FILE_KINDS.has(value.kind);
219
263
  };
220
264
  //#endregion
221
- //#region src/domain/entities/rule.ts
265
+ //#region src/core/contracts/rule.ts
222
266
  const defineRule = (rule) => rule;
223
267
  const isOptionalString = (value) => typeof value === "undefined" || typeof value === "string";
224
268
  const isConfigValueShape = (value) => typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value);
@@ -252,22 +296,26 @@ const isRuleShape = (value) => {
252
296
  return typeof value.id === "string" && typeof value.title === "string" && typeof value.description === "string" && typeof value.severity === "string" && isSeverity(value.severity) && isOptionalString(value.docs) && isProjectTypesShape(value.projectTypes) && isBindingsShape(value.bindings);
253
297
  };
254
298
  //#endregion
255
- //#region src/domain/services/pm-versions.ts
256
- /** Release history establishes stable versions, not ranges or prerelease builds. */
257
- const isStableVersion = (value) => {
258
- if (typeof value !== "string" || !/^\d/u.test(value) || value.trim() !== value) return false;
259
- const version = parse(value);
260
- return version !== null && version.prerelease.length === 0;
261
- };
262
- /** Keep name-only detection independent from whether the declaration pins a version. */
263
- const declaredPMVersion = (declaration) => {
264
- const [pm, version, ...extra] = declaration?.trim().split("@") ?? [];
265
- return pm && isPM(pm) && isStableVersion(version) && extra.length === 0 ? { [pm]: version } : {};
299
+ //#region src/core/contracts/synchronous.ts
300
+ /** Reject only the submitted async value, without a process-wide rejection handler. */
301
+ const assertSynchronous = (value, label) => {
302
+ if (value !== null && (typeof value === "object" || typeof value === "function") && "then" in value && typeof value.then === "function") {
303
+ Promise.resolve(value).catch(() => {});
304
+ throw new ConfigError(`${label} must be synchronous; Promise/thenable values are not supported.`);
305
+ }
266
306
  };
267
307
  //#endregion
268
- //#region src/domain/services/parse-siro-config.ts
308
+ //#region src/core/parse-siro-config.ts
269
309
  const RuleSettingSchema = vb.union([vb.picklist(SEVERITIES), vb.literal("off")]);
270
310
  const ConfigSchema = vb.strictObject({
311
+ exclude: vb.optional(vb.custom((value) => {
312
+ parseExcludes(value);
313
+ return true;
314
+ })),
315
+ installationRoots: vb.optional(vb.custom((value) => {
316
+ parseInstallationRoots(value);
317
+ return true;
318
+ })),
271
319
  customRules: vb.optional(vb.array(vb.custom(isRuleShape, "must be a structurally valid rule"))),
272
320
  pms: vb.optional(vb.pipe(vb.array(vb.picklist(PMS)), vb.minLength(1, "must not be empty (omit the key to auto-detect, or list at least one PM)"))),
273
321
  projectType: vb.optional(vb.picklist(PROJECT_TYPES)),
@@ -298,13 +346,14 @@ const formatIssues = (issues) => issues.map((issue) => {
298
346
  return issue.message;
299
347
  }).join("; ");
300
348
  const parseConfig = (candidate, name = "siro.config") => {
349
+ assertSynchronous(candidate, name);
301
350
  if (!isPlainRecord(candidate)) throw new ConfigError(`${name} must export a config object (got ${Array.isArray(candidate) ? "an array" : typeof candidate}).`);
302
351
  const result = vb.safeParse(ConfigSchema, Object.fromEntries(Object.entries(candidate)));
303
352
  if (!result.success) throw new ConfigError(`${name}: ${formatIssues(result.issues)}`);
304
353
  return result.output;
305
354
  };
306
355
  //#endregion
307
- //#region src/adapters/config-loader.ts
356
+ //#region src/load-config.ts
308
357
  const CONFIG_NAMES = [
309
358
  "siro.config.ts",
310
359
  "siro.config.mjs",
@@ -328,7 +377,7 @@ const loadConfig = async (cwd, nodeVersion = process.versions.node) => {
328
377
  return parseConfig(mod !== null && typeof mod === "object" && "default" in mod ? mod.default : mod, name);
329
378
  };
330
379
  //#endregion
331
- //#region src/domain/entities/config-files.ts
380
+ //#region src/core/config-files.ts
332
381
  /** Known configuration locations and their parsers. */
333
382
  const CONFIG_FILES = {
334
383
  aubeWorkspace: {
@@ -361,7 +410,7 @@ const CONFIG_FILES = {
361
410
  }
362
411
  };
363
412
  //#endregion
364
- //#region src/domain/setting-availability.ts
413
+ //#region src/core/rules/setting-availability.ts
365
414
  const { npmrc: npmrc$7, packageJson: packageJson$2, pnpmWorkspace: pnpmWorkspace$11, yarnrc: yarnrc$10, bunfig: bunfig$6 } = CONFIG_FILES;
366
415
  const settingAvailability = [
367
416
  {
@@ -482,7 +531,7 @@ const settingAvailability = [
482
531
  }
483
532
  ];
484
533
  //#endregion
485
- //#region src/domain/services/remediation-availability.ts
534
+ //#region src/core/rules/remediation-availability.ts
486
535
  const sameSetting = (setting, target) => target.file.path === setting.file.path && target.file.kind === setting.file.kind && target.keyPath.length === setting.keyPath.length && target.keyPath.every((key, index) => key === setting.keyPath[index]);
487
536
  /** Undefined means the target version or the setting's introduction is unknown. */
488
537
  const settingSupportedByTarget = (pm, version, target) => {
@@ -501,7 +550,7 @@ const guardRemediationAvailability = (pm, version, remediation, targets = remedi
501
550
  };
502
551
  };
503
552
  //#endregion
504
- //#region src/domain/rules/remediation.ts
553
+ //#region src/core/rules/remediation.ts
505
554
  /** Scalar writes must not discard an existing container or replace a non-object parent. */
506
555
  const proposeChanges = (config, operations) => {
507
556
  for (const operation of operations) {
@@ -525,7 +574,7 @@ const proposeChanges = (config, operations) => {
525
574
  };
526
575
  };
527
576
  //#endregion
528
- //#region src/domain/rules/builders/require-config-key.ts
577
+ //#region src/core/rules/builders/require-config-key.ts
529
578
  const overrideBindings = (rule, overrides) => ({
530
579
  ...rule,
531
580
  bindings: {
@@ -536,7 +585,7 @@ const overrideBindings = (rule, overrides) => ({
536
585
  const accepts = (spec, actual) => spec.accept ? spec.accept(actual) : actual === spec.value;
537
586
  const checkKeyValue = (spec, config) => {
538
587
  const actual = getByPath(config, spec.keyPath);
539
- const coveredByDefault = actual === void 0 && spec.documentedDefault !== void 0 && spec.versionNote?.defaultSafeSince === void 0 && accepts(spec, spec.documentedDefault);
588
+ const coveredByDefault = actual === void 0 && spec.documentedDefault !== void 0 && spec.defaultSafety === "unconditional" && accepts(spec, spec.documentedDefault);
540
589
  if (!coveredByDefault && accepts(spec, actual)) return { state: "ok" };
541
590
  const severity = coveredByDefault ? spec.defaultSatisfiedSeverity ?? "info" : void 0;
542
591
  if (severity === "off") return { state: "ok" };
@@ -589,7 +638,7 @@ const requireConfigKey = (options) => {
589
638
  };
590
639
  };
591
640
  //#endregion
592
- //#region src/domain/rules/builders/with-aube-paranoid.ts
641
+ //#region src/core/rules/builders/with-aube-paranoid.ts
593
642
  const withAubeParanoid = (rule) => {
594
643
  const { aube } = rule.bindings;
595
644
  if (!aube) return rule;
@@ -602,13 +651,14 @@ const withAubeParanoid = (rule) => {
602
651
  } });
603
652
  };
604
653
  //#endregion
605
- //#region src/domain/rules/advisory-check.ts
654
+ //#region src/core/rules/advisory-check.ts
606
655
  const { aubeWorkspace: aubeWorkspace$8 } = CONFIG_FILES;
607
656
  const advisoryCheck = withAubeParanoid(requireConfigKey({
608
657
  bindings: { aube: {
609
658
  accept: (value) => value === "on" || value === "required",
610
659
  docs: "https://aube.jdx.dev/security.html",
611
660
  documentedDefault: "on",
661
+ defaultSafety: "unconditional",
612
662
  file: aubeWorkspace$8,
613
663
  keyPath: ["advisoryCheck"],
614
664
  message: "Set `advisoryCheck: on` (or `required`) in aube-workspace.yaml to check dependencies against known-malicious advisories.",
@@ -621,11 +671,11 @@ const advisoryCheck = withAubeParanoid(requireConfigKey({
621
671
  title: "Enable malicious-package advisory checks"
622
672
  }));
623
673
  //#endregion
624
- //#region src/domain/rules/config-predicates.ts
674
+ //#region src/core/rules/config-predicates.ts
625
675
  const isNonBlankString = (value) => typeof value === "string" && value.trim().length > 0;
626
676
  const isStringList = (value) => Array.isArray(value) && Array.from(value).every(isNonBlankString);
627
677
  //#endregion
628
- //#region src/domain/rules/approved-git-repos.ts
678
+ //#region src/core/rules/approved-git-repos.ts
629
679
  const { yarnrc: yarnrc$9 } = CONFIG_FILES;
630
680
  const message = "Set `approvedGitRepositories: []` in .yarnrc.yml to block all git: protocol dependencies (or list approved repository globs).";
631
681
  const approvedGitRepos = defineRule({
@@ -655,7 +705,7 @@ const approvedGitRepos = defineRule({
655
705
  title: "Approve git repository dependencies"
656
706
  });
657
707
  //#endregion
658
- //#region src/domain/rules/audit-suppression.ts
708
+ //#region src/core/rules/audit-suppression.ts
659
709
  const { yarnrc: yarnrc$8 } = CONFIG_FILES;
660
710
  const isNonEmptyArray = (value) => Array.isArray(value) && value.length > 0;
661
711
  const SUPPRESSION_KEYS = ["npmAuditIgnoreAdvisories", "npmAuditExcludePackages"];
@@ -688,7 +738,7 @@ const auditSuppression = defineRule({
688
738
  title: "Review audit suppression entries"
689
739
  });
690
740
  //#endregion
691
- //#region src/domain/rules/block-auto-install.ts
741
+ //#region src/core/rules/block-auto-install.ts
692
742
  const { bunfig: bunfig$5 } = CONFIG_FILES;
693
743
  const blockAutoInstall = requireConfigKey({
694
744
  bindings: { bun: {
@@ -706,7 +756,7 @@ const blockAutoInstall = requireConfigKey({
706
756
  title: "Block automatic package installation"
707
757
  });
708
758
  //#endregion
709
- //#region src/domain/rules/block-exotic-subdeps.ts
759
+ //#region src/core/rules/block-exotic-subdeps.ts
710
760
  const { aubeWorkspace: aubeWorkspace$7, npmrc: npmrc$6, pnpmWorkspace: pnpmWorkspace$10 } = CONFIG_FILES;
711
761
  const isRootOrNone = (value) => value === "root" || value === "none";
712
762
  const npmMessage = "Set `allow-git=none` and `allow-remote=none` in .npmrc to block git/tarball URLs; use `root` only when direct URL dependencies are required.";
@@ -743,14 +793,16 @@ const blockExoticSubdeps = overrideBindings(requireConfigKey({
743
793
  aube: {
744
794
  docs: "https://aube.jdx.dev/security.html",
745
795
  documentedDefault: true,
796
+ defaultSafety: "unconditional",
746
797
  file: aubeWorkspace$7,
747
798
  keyPath: ["blockExoticSubdeps"],
748
799
  message: "Set `blockExoticSubdeps: true` in aube-workspace.yaml to block git/tarball URLs in transitive dependencies.",
749
800
  value: true
750
801
  },
751
802
  pnpm: {
752
- docs: "https://pnpm.io/settings#blockexoticsubdeps",
803
+ docs: "https://pnpm.io/settings/dependency-resolution#blockexoticsubdeps",
753
804
  documentedDefault: true,
805
+ defaultSafety: "conditional",
754
806
  file: pnpmWorkspace$10,
755
807
  keyPath: ["blockExoticSubdeps"],
756
808
  message: "Set `blockExoticSubdeps: true` in pnpm-workspace.yaml to block git/tarball URLs in transitive dependencies.",
@@ -762,13 +814,13 @@ const blockExoticSubdeps = overrideBindings(requireConfigKey({
762
814
  }
763
815
  },
764
816
  description: "Refuse to install transitive dependencies sourced from git or tarball URLs, which bypass registry integrity checking.",
765
- docs: "https://pnpm.io/settings#blockexoticsubdeps",
817
+ docs: "https://pnpm.io/settings/dependency-resolution#blockexoticsubdeps",
766
818
  id: "block-exotic-subdeps",
767
819
  severity: "warn",
768
820
  title: "Block exotic subdependencies"
769
821
  }), { npm: npmBinding$1 });
770
822
  //#endregion
771
- //#region src/domain/rules/bun-security-scanner.ts
823
+ //#region src/core/rules/bun-security-scanner.ts
772
824
  const { bunfig: bunfig$4 } = CONFIG_FILES;
773
825
  const bunSecurityScanner = defineRule({
774
826
  bindings: { bun: {
@@ -807,12 +859,13 @@ const bunSecurityScanner = defineRule({
807
859
  title: "Enable a bun install-time security scanner"
808
860
  });
809
861
  //#endregion
810
- //#region src/domain/rules/checksum-verification.ts
862
+ //#region src/core/rules/checksum-verification.ts
811
863
  const { yarnrc: yarnrc$7 } = CONFIG_FILES;
812
864
  const checksumVerification = requireConfigKey({
813
865
  bindings: { yarn: {
814
866
  docs: "https://yarnpkg.com/configuration/yarnrc#checksumBehavior",
815
867
  documentedDefault: "throw",
868
+ defaultSafety: "unconditional",
816
869
  file: yarnrc$7,
817
870
  keyPath: ["checksumBehavior"],
818
871
  message: "Set `checksumBehavior: throw` in .yarnrc.yml to reject packages whose checksum does not match the lockfile.",
@@ -825,7 +878,7 @@ const checksumVerification = requireConfigKey({
825
878
  title: "Verify package checksums"
826
879
  });
827
880
  //#endregion
828
- //#region src/domain/entities/signals.ts
881
+ //#region src/core/signals.ts
829
882
  const PM_SIGNALS = {
830
883
  aube: {
831
884
  configs: [CONFIG_FILES.aubeWorkspace.path],
@@ -860,11 +913,11 @@ const PM_SIGNALS = {
860
913
  }
861
914
  };
862
915
  //#endregion
863
- //#region src/domain/rules/commit-lockfile.ts
916
+ //#region src/core/rules/commit-lockfile.ts
864
917
  const LOCKFILE_DOCS = {
865
918
  aube: "https://github.com/aubepkg/aube/blob/main/docs/package-manager/lockfiles.md",
866
919
  bun: "https://bun.com/docs/install/lockfile",
867
- deno: "https://docs.deno.com/runtime/fundamentals/modules/#integrity-checking-and-lock-files",
920
+ deno: "https://docs.deno.com/runtime/reference/deno_json/#lockfile",
868
921
  npm: "https://docs.npmjs.com/cli/v12/configuring-npm/package-lock-json",
869
922
  pnpm: "https://pnpm.io/git#lockfiles",
870
923
  yarn: "https://yarnpkg.com/getting-started/qa#should-lockfiles-be-committed-to-the-repository"
@@ -873,7 +926,7 @@ const LOCKFILE_VERSION_NOTES = { deno: { configAvailableSince: "deno 1.28.0" } }
873
926
  const lockfileBinding = (pm) => {
874
927
  const { lockfiles, reusesLockfiles } = PM_SIGNALS[pm];
875
928
  const [primary] = lockfiles;
876
- const accepted = [...lockfiles, ...reusesLockfiles ?? []].filter((lockfile) => pm !== "npm" || lockfile === primary);
929
+ const accepted = [...lockfiles, ...reusesLockfiles ?? []];
877
930
  return {
878
931
  check(ctx) {
879
932
  if (pm === "aube" && ctx.exists(asRelPath("bun.lockb")) && !ctx.exists(asRelPath("bun.lock"))) return {
@@ -884,7 +937,20 @@ const lockfileBinding = (pm) => {
884
937
  steps: ["Run `bun install --save-text-lockfile` to generate bun.lock, then commit it."]
885
938
  }
886
939
  };
887
- if (accepted.some((lf) => ctx.exists(asRelPath(lf)))) return { state: "ok" };
940
+ if (pm === "npm") {
941
+ if (ctx.exists(asRelPath(primary))) return { state: "ok" };
942
+ if (ctx.exists(asRelPath("npm-shrinkwrap.json"))) {
943
+ if (ctx.pmVersion && lt(ctx.pmVersion, "12.0.0")) return { state: "ok" };
944
+ return {
945
+ state: "violation",
946
+ message: ctx.pmVersion ? `npm ${ctx.pmVersion} no longer reads npm-shrinkwrap.json. Use package-lock.json.` : "Found npm-shrinkwrap.json, but the npm target version is unknown; npm 12 no longer reads it.",
947
+ remediation: {
948
+ kind: "manual",
949
+ steps: ["Verify and declare the npm version used by this project. For npm 12 or newer, migrate to package-lock.json and review the resulting dependency resolutions."]
950
+ }
951
+ };
952
+ }
953
+ } else if (accepted.some((lf) => ctx.exists(asRelPath(lf)))) return { state: "ok" };
888
954
  return {
889
955
  remediation: {
890
956
  kind: "manual",
@@ -935,13 +1001,13 @@ const commitLockfile = defineRule({
935
1001
  yarn: lockfileBinding("yarn")
936
1002
  },
937
1003
  description: "Lockfiles pin the full dependency tree and integrity hashes, enabling reproducible, verifiable installs (e.g. `npm ci`).",
938
- docs: "https://github.com/bodadotsh/npm-security-best-practices#2-include-lockfiles",
1004
+ docs: "https://github.com/bodadotsh/npm-security-best-practices#3-include-lockfiles",
939
1005
  id: "commit-lockfile",
940
1006
  severity: "error",
941
1007
  title: "Commit a lockfile"
942
1008
  });
943
1009
  //#endregion
944
- //#region src/domain/rules/dependency-overrides.ts
1010
+ //#region src/core/rules/dependency-overrides.ts
945
1011
  const { aubeWorkspace: aubeWorkspace$6, pnpmWorkspace: pnpmWorkspace$9 } = CONFIG_FILES;
946
1012
  const isNonEmptyObject$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
947
1013
  const checkOverrides = (config, file) => {
@@ -967,16 +1033,16 @@ const makeBinding = (file, fileName, docs) => ({
967
1033
  const dependencyOverrides = defineRule({
968
1034
  bindings: {
969
1035
  aube: makeBinding(aubeWorkspace$6, "aube-workspace.yaml", "https://aube.sh/settings/"),
970
- pnpm: makeBinding(pnpmWorkspace$9, "pnpm-workspace.yaml", "https://pnpm.io/settings#overrides")
1036
+ pnpm: makeBinding(pnpmWorkspace$9, "pnpm-workspace.yaml", "https://pnpm.io/settings/dependency-resolution#overrides")
971
1037
  },
972
1038
  description: "Flag dependency overrides that can replace transitive packages with arbitrary versions or forks — a supply-chain injection vector.",
973
- docs: "https://pnpm.io/settings#overrides",
1039
+ docs: "https://pnpm.io/settings/dependency-resolution#overrides",
974
1040
  id: "dependency-overrides",
975
1041
  severity: "info",
976
1042
  title: "Review dependency overrides"
977
1043
  });
978
1044
  //#endregion
979
- //#region src/domain/rules/disable-lifecycle-scripts.ts
1045
+ //#region src/core/rules/disable-lifecycle-scripts.ts
980
1046
  const { npmrc: npmrc$5, pnpmWorkspace: pnpmWorkspace$8, yarnrc: yarnrc$6, aubeWorkspace: aubeWorkspace$5, bunfig: bunfig$3 } = CONFIG_FILES;
981
1047
  const pnpmBinding$4 = {
982
1048
  check(ctx, config) {
@@ -1050,7 +1116,7 @@ const pnpmBinding$4 = {
1050
1116
  }])
1051
1117
  };
1052
1118
  },
1053
- docs: "https://pnpm.io/settings#strictdepbuilds",
1119
+ docs: "https://pnpm.io/settings/build#strictdepbuilds",
1054
1120
  file: pnpmWorkspace$8,
1055
1121
  versionNote: {
1056
1122
  configAvailableSince: "pnpm 10.3.0",
@@ -1137,6 +1203,7 @@ const disableLifecycleScripts = withAubeParanoid(overrideBindings(requireConfigK
1137
1203
  yarn: {
1138
1204
  docs: "https://yarnpkg.com/configuration/yarnrc#enableScripts",
1139
1205
  documentedDefault: false,
1206
+ defaultSafety: "conditional",
1140
1207
  file: yarnrc$6,
1141
1208
  keyPath: ["enableScripts"],
1142
1209
  message: "Set `enableScripts: false` in .yarnrc.yml to pin the policy across versions.",
@@ -1148,7 +1215,7 @@ const disableLifecycleScripts = withAubeParanoid(overrideBindings(requireConfigK
1148
1215
  }
1149
1216
  },
1150
1217
  description: "Malicious postinstall scripts are a primary supply-chain attack vector. Prevent automatic execution of dependency lifecycle scripts.",
1151
- docs: "https://github.com/bodadotsh/npm-security-best-practices#3-disable-lifecycle-scripts",
1218
+ docs: "https://github.com/bodadotsh/npm-security-best-practices#1-disable-lifecycle-scripts",
1152
1219
  id: "disable-lifecycle-scripts",
1153
1220
  severity: "error",
1154
1221
  title: "Disable dependency lifecycle scripts"
@@ -1158,7 +1225,7 @@ const disableLifecycleScripts = withAubeParanoid(overrideBindings(requireConfigK
1158
1225
  pnpm: pnpmBinding$4
1159
1226
  }));
1160
1227
  //#endregion
1161
- //#region src/domain/rules/enforce-strict-ssl.ts
1228
+ //#region src/core/rules/enforce-strict-ssl.ts
1162
1229
  const { npmrc: npmrc$4, yarnrc: yarnrc$5 } = CONFIG_FILES;
1163
1230
  const yarnMessage = "Set `enableStrictSsl: true` in .yarnrc.yml to enforce SSL certificate validation for registry connections.";
1164
1231
  const yarnBinding = {
@@ -1197,6 +1264,7 @@ const enforceStrictSsl = overrideBindings(requireConfigKey({
1197
1264
  bindings: { npm: {
1198
1265
  docs: "https://docs.npmjs.com/cli/v11/using-npm/config#strict-ssl",
1199
1266
  documentedDefault: true,
1267
+ defaultSafety: "unconditional",
1200
1268
  file: npmrc$4,
1201
1269
  keyPath: ["strict-ssl"],
1202
1270
  message: "Set `strict-ssl=true` in .npmrc to enforce SSL certificate validation.",
@@ -1209,7 +1277,7 @@ const enforceStrictSsl = overrideBindings(requireConfigKey({
1209
1277
  title: "Enforce SSL for registry connections"
1210
1278
  }), { yarn: yarnBinding });
1211
1279
  //#endregion
1212
- //#region src/domain/services/project-type.ts
1280
+ //#region src/core/resolve-project-type.ts
1213
1281
  const isPublishableName = (value) => typeof value === "string" && value.trim().length > 0;
1214
1282
  const resolveProjectType = (selected, inferredPackage) => selected ?? (inferredPackage ? "package" : "application");
1215
1283
  const resolvePackageJsonProjectType = (ctx) => {
@@ -1219,10 +1287,10 @@ const resolvePackageJsonProjectType = (ctx) => {
1219
1287
  };
1220
1288
  const resolveDenoProjectType = (ctx, denoConfig) => resolveProjectType(ctx.projectType, getByPath(denoConfig, ["publish"]) !== false && isPublishableName(getByPath(denoConfig, ["name"])));
1221
1289
  //#endregion
1222
- //#region src/domain/rules/publishable.ts
1290
+ //#region src/core/rules/publishable.ts
1223
1291
  const isPublishable = (ctx) => resolvePackageJsonProjectType(ctx) === "package";
1224
1292
  //#endregion
1225
- //#region src/domain/rules/files-field.ts
1293
+ //#region src/core/rules/files-field.ts
1226
1294
  const { packageJson: packageJson$1, denoJson: denoJson$3 } = CONFIG_FILES;
1227
1295
  const packageJsonFilesBinding = {
1228
1296
  check(ctx) {
@@ -1242,47 +1310,48 @@ const packageJsonFilesBinding = {
1242
1310
  docs: "https://docs.npmjs.com/cli/v11/configuring-npm/package-json#files",
1243
1311
  file: packageJson$1
1244
1312
  };
1313
+ const denoPublishBinding = {
1314
+ check(ctx, config) {
1315
+ if (resolveDenoProjectType(ctx, config) !== "package") return { state: "na" };
1316
+ const include = getByPath(config, ["publish", "include"]);
1317
+ if (isStringList(include) && include.length > 0) return { state: "ok" };
1318
+ return {
1319
+ remediation: {
1320
+ kind: "manual",
1321
+ steps: ["Add a non-empty `publish.include` array to deno.json; use `publish.exclude` for additional exclusions."]
1322
+ },
1323
+ actual: include,
1324
+ message: "Add `publish.include` to deno.json to limit what gets published.",
1325
+ state: "violation"
1326
+ };
1327
+ },
1328
+ docs: "https://docs.deno.com/runtime/reference/deno_json/#publish---override-.gitignore",
1329
+ file: denoJson$3
1330
+ };
1245
1331
  const filesField = defineRule({
1246
1332
  bindings: {
1247
1333
  aube: packageJsonFilesBinding,
1248
1334
  bun: packageJsonFilesBinding,
1249
- deno: {
1250
- check(ctx, config) {
1251
- if (resolveDenoProjectType(ctx, config) !== "package") return { state: "na" };
1252
- const include = getByPath(config, ["publish", "include"]);
1253
- if (isStringList(include) && include.length > 0) return { state: "ok" };
1254
- return {
1255
- remediation: {
1256
- kind: "manual",
1257
- steps: ["Add a non-empty `publish.include` array to deno.json; use `publish.exclude` for additional exclusions."]
1258
- },
1259
- actual: include,
1260
- message: "Add `publish.include` to deno.json to limit what gets published.",
1261
- state: "violation"
1262
- };
1263
- },
1264
- docs: "https://docs.deno.com/runtime/reference/cli/publish/#how-publishing-works",
1265
- file: denoJson$3
1266
- },
1335
+ deno: denoPublishBinding,
1267
1336
  npm: packageJsonFilesBinding,
1268
1337
  pnpm: packageJsonFilesBinding,
1269
1338
  yarn: packageJsonFilesBinding
1270
1339
  },
1271
1340
  description: "An explicit `files` array in package.json restricts what gets published, preventing accidental inclusion of secrets or local files.",
1272
- docs: "https://github.com/bodadotsh/npm-security-best-practices#11-review-published-files",
1341
+ docs: "https://github.com/bodadotsh/npm-security-best-practices#12-review-published-files",
1273
1342
  id: "files-field",
1274
1343
  projectTypes: ["package"],
1275
1344
  severity: "info",
1276
1345
  title: "Declare a published files allow-list"
1277
1346
  });
1278
1347
  //#endregion
1279
- //#region src/domain/rules/frozen-lockfile.ts
1348
+ //#region src/core/rules/frozen-lockfile.ts
1280
1349
  const { pnpmWorkspace: pnpmWorkspace$7, yarnrc: yarnrc$4, bunfig: bunfig$2, denoJson: denoJson$2 } = CONFIG_FILES;
1281
1350
  const aubeFrozenCommand = "Use `aube ci` or `aube install --frozen-lockfile` to fail when the lockfile would change.";
1282
1351
  const frozenLockfile = overrideBindings(requireConfigKey({
1283
1352
  bindings: {
1284
1353
  deno: {
1285
- docs: "https://docs.deno.com/runtime/fundamentals/configuration/#lock",
1354
+ docs: "https://docs.deno.com/runtime/reference/deno_json/#lockfile",
1286
1355
  file: denoJson$2,
1287
1356
  keyPath: ["lock", "frozen"],
1288
1357
  message: "Set `lock.frozen: true` in deno.json for reproducible, verified installs.",
@@ -1297,7 +1366,7 @@ const frozenLockfile = overrideBindings(requireConfigKey({
1297
1366
  versionNote: { configAvailableSince: "bun 0.6.10" }
1298
1367
  },
1299
1368
  pnpm: {
1300
- docs: "https://pnpm.io/settings#frozenlockfile",
1369
+ docs: "https://pnpm.io/cli/install#--frozen-lockfile",
1301
1370
  file: pnpmWorkspace$7,
1302
1371
  keyPath: ["frozenLockfile"],
1303
1372
  message: "pnpm defaults frozenLockfile to true in CI — set it explicitly in pnpm-workspace.yaml to pin the policy outside CI too.",
@@ -1306,6 +1375,7 @@ const frozenLockfile = overrideBindings(requireConfigKey({
1306
1375
  yarn: {
1307
1376
  docs: "https://yarnpkg.com/configuration/yarnrc#enableImmutableInstalls",
1308
1377
  documentedDefault: true,
1378
+ defaultSafety: "conditional",
1309
1379
  file: yarnrc$4,
1310
1380
  keyPath: ["enableImmutableInstalls"],
1311
1381
  message: "Set `enableImmutableInstalls: true` in .yarnrc.yml to pin the policy outside CI too.",
@@ -1317,7 +1387,7 @@ const frozenLockfile = overrideBindings(requireConfigKey({
1317
1387
  }
1318
1388
  },
1319
1389
  description: "Refuse to mutate the lockfile on install so unexpected dependency changes fail loudly.",
1320
- docs: "https://github.com/bodadotsh/npm-security-best-practices#2-include-lockfiles",
1390
+ docs: "https://github.com/bodadotsh/npm-security-best-practices#3-include-lockfiles",
1321
1391
  id: "frozen-lockfile",
1322
1392
  severity: "warn",
1323
1393
  title: "Freeze the lockfile"
@@ -1336,7 +1406,7 @@ const frozenLockfile = overrideBindings(requireConfigKey({
1336
1406
  severity: "info"
1337
1407
  } });
1338
1408
  //#endregion
1339
- //#region src/domain/rules/frozen-store.ts
1409
+ //#region src/core/rules/frozen-store.ts
1340
1410
  const { pnpmWorkspace: pnpmWorkspace$6 } = CONFIG_FILES;
1341
1411
  const frozenStore = defineRule({
1342
1412
  bindings: { pnpm: {
@@ -1367,12 +1437,13 @@ const frozenStore = defineRule({
1367
1437
  title: "Consider a pre-populated read-only store"
1368
1438
  });
1369
1439
  //#endregion
1370
- //#region src/domain/rules/hardened-mode.ts
1440
+ //#region src/core/rules/hardened-mode.ts
1371
1441
  const { yarnrc: yarnrc$3 } = CONFIG_FILES;
1372
1442
  const hardenedMode = requireConfigKey({
1373
1443
  bindings: { yarn: {
1374
1444
  docs: "https://yarnpkg.com/configuration/yarnrc#enableHardenedMode",
1375
1445
  documentedDefault: true,
1446
+ defaultSafety: "conditional",
1376
1447
  file: yarnrc$3,
1377
1448
  keyPath: ["enableHardenedMode"],
1378
1449
  message: "yarn enables hardened mode automatically only for pull requests on public repositories — set `enableHardenedMode: true` in .yarnrc.yml to extend end-to-end install verification to every install.",
@@ -1389,7 +1460,7 @@ const hardenedMode = requireConfigKey({
1389
1460
  title: "Enable hardened mode (yarn)"
1390
1461
  });
1391
1462
  //#endregion
1392
- //#region src/domain/services/deno-release-age.ts
1463
+ //#region src/core/rules/deno-release-age.ts
1393
1464
  const DENO_DURATION = /^\+?P(?:\+?\d+[Ww]|(?:\d+[Dd])*(?:T(?:\d+[HhMm]|\d+(?:\.\d+)?[Ss])+)?)$/u;
1394
1465
  const DENO_MIN_TIMESTAMP = Date.UTC(-262143, 0, 1);
1395
1466
  const DENO_UNIT_SECONDS = {
@@ -1427,7 +1498,7 @@ const isActiveDenoReleaseAge = (value, now, parse) => {
1427
1498
  return !Number.isNaN(midnight.valueOf()) && midnight.toISOString().slice(0, 10) === date && timestamp < now;
1428
1499
  };
1429
1500
  //#endregion
1430
- //#region src/domain/rules/minimum-release-age.ts
1501
+ //#region src/core/rules/minimum-release-age.ts
1431
1502
  const HOURS_PER_DAY = 24;
1432
1503
  const MINUTES_PER_HOUR = 60;
1433
1504
  const SECONDS_PER_MINUTE = 60;
@@ -1444,7 +1515,7 @@ const isNonDisabledDenoReleaseAge = (value, now, parse) => {
1444
1515
  if (!isPlainRecord(value)) return isActiveDenoReleaseAge(value, now, parse);
1445
1516
  if (Object.keys(value).some((key) => key !== "age" && key !== "exclude")) return false;
1446
1517
  if (value.exclude !== void 0 && !isStringList(value.exclude)) return false;
1447
- return value.age == null || isActiveDenoReleaseAge(value.age, now, parse);
1518
+ return value.age != null && isActiveDenoReleaseAge(value.age, now, parse);
1448
1519
  };
1449
1520
  const denoAgeUsesFallback = (value) => {
1450
1521
  if (value === void 0 || value === null) return true;
@@ -1453,13 +1524,14 @@ const denoAgeUsesFallback = (value) => {
1453
1524
  if (value.exclude !== void 0 && !isStringList(value.exclude)) return false;
1454
1525
  return value.age == null;
1455
1526
  };
1456
- const isPositiveDenoNpmrcDays = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0;
1457
- const baseRule$1 = requireConfigKey({
1527
+ const isPositiveDenoNpmrcDays = (value, now, parse) => typeof value === "number" && Number.isSafeInteger(value) && isActiveDenoReleaseAge(value * MINUTES_PER_DAY, now, parse);
1528
+ const baseRule$2 = requireConfigKey({
1458
1529
  bindings: {
1459
1530
  aube: {
1460
1531
  accept: isPositiveNumber,
1461
1532
  docs: "https://aube.sh/settings/",
1462
1533
  documentedDefault: DOCUMENTED_DEFAULT_MINUTES,
1534
+ defaultSafety: "unconditional",
1463
1535
  file: aubeWorkspace$4,
1464
1536
  keyPath: ["minimumReleaseAge"],
1465
1537
  message: `Set minimumReleaseAge (~${RECOMMENDED_RELEASE_AGE_MINUTES} minutes for a 3-day cooldown) in aube-workspace.yaml.`,
@@ -1476,8 +1548,9 @@ const baseRule$1 = requireConfigKey({
1476
1548
  },
1477
1549
  pnpm: {
1478
1550
  accept: isPositiveNumber,
1479
- docs: "https://pnpm.io/settings#minimumreleaseage",
1551
+ docs: "https://pnpm.io/settings/dependency-resolution#minimumreleaseage",
1480
1552
  documentedDefault: DOCUMENTED_DEFAULT_MINUTES,
1553
+ defaultSafety: "conditional",
1481
1554
  file: pnpmWorkspace$5,
1482
1555
  keyPath: ["minimumReleaseAge"],
1483
1556
  message: `Set minimumReleaseAge (~${RECOMMENDED_RELEASE_AGE_MINUTES} minutes for a 3-day cooldown) in pnpm-workspace.yaml.`,
@@ -1491,6 +1564,7 @@ const baseRule$1 = requireConfigKey({
1491
1564
  accept: isPositiveYarnDuration,
1492
1565
  docs: "https://yarnpkg.com/configuration/yarnrc#npmMinimalAgeGate",
1493
1566
  documentedDefault: DOCUMENTED_DEFAULT_MINUTES,
1567
+ defaultSafety: "conditional",
1494
1568
  file: yarnrc$2,
1495
1569
  keyPath: ["npmMinimalAgeGate"],
1496
1570
  message: `Set npmMinimalAgeGate (~${RECOMMENDED_RELEASE_AGE_MINUTES} minutes for a 3-day cooldown) in .yarnrc.yml.`,
@@ -1502,7 +1576,7 @@ const baseRule$1 = requireConfigKey({
1502
1576
  }
1503
1577
  },
1504
1578
  description: "Refuse to install releases newer than a cooldown window so freshly published (possibly compromised) versions are skipped.",
1505
- docs: "https://github.com/bodadotsh/npm-security-best-practices#set-minimal-release-age",
1579
+ docs: "https://github.com/bodadotsh/npm-security-best-practices#2-set-cooldowns--minimum-release-age",
1506
1580
  id: "minimum-release-age",
1507
1581
  severity: "warn",
1508
1582
  title: "Set a minimum release age"
@@ -1563,10 +1637,11 @@ const createDenoBinding = (time) => ({
1563
1637
  },
1564
1638
  check(ctx, config) {
1565
1639
  const actual = getByPath(config, ["minimumDependencyAge"]);
1640
+ const now = time.now();
1566
1641
  if (denoAgeUsesFallback(actual)) {
1567
1642
  const npmrcConfig = ctx.readConfig(npmrc$3);
1568
1643
  const npmrcAge = getByPath(npmrcConfig, ["min-release-age"]);
1569
- if (isPositiveDenoNpmrcDays(npmrcAge)) return { state: "ok" };
1644
+ if (isPositiveDenoNpmrcDays(npmrcAge, now, time.parse)) return { state: "ok" };
1570
1645
  if (npmrcAge === 0) return {
1571
1646
  state: "violation",
1572
1647
  actual: npmrcAge,
@@ -1581,7 +1656,7 @@ const createDenoBinding = (time) => ({
1581
1656
  }])
1582
1657
  };
1583
1658
  }
1584
- if (isNonDisabledDenoReleaseAge(actual, time.now(), time.parse)) return { state: "ok" };
1659
+ if (isNonDisabledDenoReleaseAge(actual, now, time.parse)) return { state: "ok" };
1585
1660
  const objectAge = isPlainRecord(actual);
1586
1661
  const invalidObject = objectAge && (Object.keys(actual).some((key) => key !== "age" && key !== "exclude") || actual.exclude !== void 0 && !isStringList(actual.exclude));
1587
1662
  return {
@@ -1601,12 +1676,12 @@ const createDenoBinding = (time) => ({
1601
1676
  };
1602
1677
  }
1603
1678
  });
1604
- const createMinimumReleaseAge = (time) => overrideBindings(baseRule$1, {
1679
+ const createMinimumReleaseAge = (time) => overrideBindings(baseRule$2, {
1605
1680
  npm: createNpmBinding(time),
1606
1681
  deno: createDenoBinding(time)
1607
1682
  });
1608
1683
  //#endregion
1609
- //#region src/domain/rules/named-registries.ts
1684
+ //#region src/core/rules/named-registries.ts
1610
1685
  const { pnpmWorkspace: pnpmWorkspace$4 } = CONFIG_FILES;
1611
1686
  const isNonEmptyObject$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
1612
1687
  const namedRegistries = defineRule({
@@ -1624,17 +1699,17 @@ const namedRegistries = defineRule({
1624
1699
  state: "violation"
1625
1700
  };
1626
1701
  },
1627
- docs: "https://pnpm.io/settings#namedregistries",
1702
+ docs: "https://pnpm.io/settings/dependency-resolution#namedregistries",
1628
1703
  file: pnpmWorkspace$4
1629
1704
  } },
1630
1705
  description: "Flag named registry mappings that redirect package resolution to custom registries, which may enable dependency confusion attacks.",
1631
- docs: "https://pnpm.io/settings#namedregistries",
1706
+ docs: "https://pnpm.io/settings/dependency-resolution#namedregistries",
1632
1707
  id: "named-registries",
1633
1708
  severity: "info",
1634
1709
  title: "Review named registry mappings"
1635
1710
  });
1636
1711
  //#endregion
1637
- //#region src/domain/rules/paranoid-mode.ts
1712
+ //#region src/core/rules/paranoid-mode.ts
1638
1713
  const { aubeWorkspace: aubeWorkspace$3 } = CONFIG_FILES;
1639
1714
  const paranoidMode = requireConfigKey({
1640
1715
  bindings: { aube: {
@@ -1651,7 +1726,7 @@ const paranoidMode = requireConfigKey({
1651
1726
  title: "Enable paranoid security mode"
1652
1727
  });
1653
1728
  //#endregion
1654
- //#region src/domain/rules/patched-dependencies.ts
1729
+ //#region src/core/rules/patched-dependencies.ts
1655
1730
  const { pnpmWorkspace: pnpmWorkspace$3 } = CONFIG_FILES;
1656
1731
  const isNonEmptyObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
1657
1732
  const patchedDependencies = defineRule({
@@ -1669,17 +1744,17 @@ const patchedDependencies = defineRule({
1669
1744
  state: "violation"
1670
1745
  };
1671
1746
  },
1672
- docs: "https://pnpm.io/settings#patcheddependencies",
1747
+ docs: "https://pnpm.io/cli/patch#patcheddependencies",
1673
1748
  file: pnpmWorkspace$3
1674
1749
  } },
1675
1750
  description: "Review local patches separately from the registry artifacts they modify.",
1676
- docs: "https://pnpm.io/settings#patcheddependencies",
1751
+ docs: "https://pnpm.io/cli/patch#patcheddependencies",
1677
1752
  id: "patched-dependencies",
1678
1753
  severity: "info",
1679
1754
  title: "Review patched dependencies"
1680
1755
  });
1681
1756
  //#endregion
1682
- //#region src/domain/rules/pin-exact-versions.ts
1757
+ //#region src/core/rules/pin-exact-versions.ts
1683
1758
  const { npmrc: npmrc$2, pnpmWorkspace: pnpmWorkspace$2, yarnrc: yarnrc$1, bunfig, denoJson } = CONFIG_FILES;
1684
1759
  const REGISTRY_VERSION = /^(?:npm|jsr):(?:@[^/@]+\/)?[^/@]+@(?<version>[^/]*)/u;
1685
1760
  const isUnpinnedRegistryImport = (specifier) => {
@@ -1688,9 +1763,14 @@ const isUnpinnedRegistryImport = (specifier) => {
1688
1763
  const normalized = version?.startsWith("=") ? version.slice(1) : version;
1689
1764
  return normalized === void 0 || valid(normalized) === null;
1690
1765
  };
1691
- const collectUnpinnedImports = (imports) => {
1766
+ const collectUnpinnedImports = (imports, location) => {
1767
+ if (!isPlainRecord(imports)) throw new ConfigError(`deno.json: ${location} must be an import mapping object.`);
1692
1768
  const offenders = [];
1693
- for (const [name, value] of Object.entries(imports)) if (typeof value === "string" && isUnpinnedRegistryImport(value)) offenders.push(`${name}=${value}`);
1769
+ for (const [name, value] of Object.entries(imports)) {
1770
+ if (value === null) continue;
1771
+ if (typeof value !== "string") throw new ConfigError(`deno.json: ${location}.${name} must be a string or null.`);
1772
+ if (isUnpinnedRegistryImport(value)) offenders.push(`${location}.${name}=${value}`);
1773
+ }
1694
1774
  return offenders;
1695
1775
  };
1696
1776
  const MAX_SAMPLE_COUNT = 3;
@@ -1701,23 +1781,26 @@ const formatOffenders = (offenders) => {
1701
1781
  return {
1702
1782
  remediation: {
1703
1783
  kind: "manual",
1704
- steps: ["Run `deno add --save-exact <pkg>` for each unpinned registry import in deno.json, or rewrite the `imports` entries to use exact versions."]
1784
+ steps: ["Pin each reported imports/scopes entry in deno.json to an exact version. For top-level imports, `deno add --save-exact <pkg>` can help; edit scoped mappings at their reported locations."]
1705
1785
  },
1706
1786
  message: `${offenders.length} deno imports are not pinned: ${sample}${more}. Use \`deno add --save-exact\` or pin manually.`,
1707
1787
  state: "violation"
1708
1788
  };
1709
1789
  };
1710
1790
  const OK = { state: "ok" };
1711
- const extractImportsRecord = (config) => {
1791
+ const unpinnedInlineImports = (config) => {
1712
1792
  const imports = getByPath(config, ["imports"]);
1713
- if (!isPlainRecord(imports)) return;
1714
- return imports;
1793
+ const offenders = imports == null ? [] : collectUnpinnedImports(imports, "imports");
1794
+ const scopes = getByPath(config, ["scopes"]);
1795
+ if (scopes != null) {
1796
+ if (!isPlainRecord(scopes)) throw new ConfigError("deno.json: scopes must be an object.");
1797
+ for (const [scope, mapping] of Object.entries(scopes)) offenders.push(...collectUnpinnedImports(mapping, `scopes[${JSON.stringify(scope)}]`));
1798
+ }
1799
+ return offenders;
1715
1800
  };
1716
1801
  const denoBinding = {
1717
1802
  check(_ctx, config) {
1718
- const imports = extractImportsRecord(config);
1719
- if (!imports) return OK;
1720
- const offenders = collectUnpinnedImports(imports);
1803
+ const offenders = unpinnedInlineImports(config);
1721
1804
  if (offenders.length === 0) return OK;
1722
1805
  return formatOffenders(offenders);
1723
1806
  },
@@ -1748,7 +1831,7 @@ const npmBinding = {
1748
1831
  };
1749
1832
  const aubeBinding = {
1750
1833
  file: npmrc$2,
1751
- docs: "https://aube.jdx.dev/settings/#saveprefix",
1834
+ docs: "https://aube.sh/settings/#setting-saveprefix",
1752
1835
  check(_ctx, config) {
1753
1836
  const prefixes = ["save-prefix", "savePrefix"].filter((key) => Object.hasOwn(config, key)).map((key) => config[key]);
1754
1837
  if (prefixes.length > 0 && prefixes.every((value) => value === "")) return OK;
@@ -1789,8 +1872,8 @@ const pinExactVersions = overrideBindings(requireConfigKey({
1789
1872
  versionNote: { configAvailableSince: "yarn 2.0.0" }
1790
1873
  }
1791
1874
  },
1792
- description: "Semver ranges (^, ~) auto-adopt new releases, including compromised ones. Save exact versions by default.",
1793
- docs: "https://github.com/bodadotsh/npm-security-best-practices#1-pin-dependency-versions",
1875
+ description: "Semver ranges (^, ~) auto-adopt new releases, including compromised ones. Save exact versions by default; for Deno, inspect registry mappings in both inline imports and scopes (not external import maps).",
1876
+ docs: "https://github.com/bodadotsh/npm-security-best-practices#4-pin-dependency-versions",
1794
1877
  id: "pin-exact-versions",
1795
1878
  severity: "error",
1796
1879
  title: "Pin exact dependency versions"
@@ -1800,7 +1883,7 @@ const pinExactVersions = overrideBindings(requireConfigKey({
1800
1883
  npm: npmBinding
1801
1884
  });
1802
1885
  //#endregion
1803
- //#region src/domain/rules/provenance.ts
1886
+ //#region src/core/rules/provenance.ts
1804
1887
  const { npmrc: npmrc$1, yarnrc } = CONFIG_FILES;
1805
1888
  const npmrcProvenance = {
1806
1889
  file: npmrc$1,
@@ -1808,7 +1891,7 @@ const npmrcProvenance = {
1808
1891
  message: "Set `provenance=true` (and publish from CI) to attest releases.",
1809
1892
  value: true
1810
1893
  };
1811
- const provenance = requireConfigKey({
1894
+ const baseRule = requireConfigKey({
1812
1895
  applies: isPublishable,
1813
1896
  bindings: {
1814
1897
  bun: {
@@ -1834,14 +1917,37 @@ const provenance = requireConfigKey({
1834
1917
  }
1835
1918
  },
1836
1919
  description: "Provenance statements (via Sigstore) bind a published artifact to its recorded source and build.",
1837
- docs: "https://github.com/bodadotsh/npm-security-best-practices#10-generate-provenance-statements",
1920
+ docs: "https://github.com/bodadotsh/npm-security-best-practices#11-generate-provenance-statements",
1838
1921
  id: "provenance",
1839
1922
  projectTypes: ["package"],
1840
1923
  severity: "warn",
1841
1924
  title: "Publish with provenance"
1842
1925
  });
1926
+ const provenance = overrideBindings(baseRule, { npm: {
1927
+ ...baseRule.bindings.npm,
1928
+ check(ctx, config) {
1929
+ if (!isPublishable(ctx)) return { state: "na" };
1930
+ const publishConfig = ctx.packageJson?.publishConfig;
1931
+ if (!publishConfig || !Object.hasOwn(publishConfig, "provenance")) return baseRule.bindings.npm.check(ctx, config);
1932
+ if (publishConfig.provenance === true) return { state: "ok" };
1933
+ const file = CONFIG_FILES.packageJson;
1934
+ return {
1935
+ state: "violation",
1936
+ file: file.path,
1937
+ actual: publishConfig.provenance,
1938
+ expected: true,
1939
+ message: "Set publishConfig.provenance=true in package.json (and publish from CI) to attest releases.",
1940
+ remediation: guardRemediationAvailability("npm", ctx.pmVersion, proposeChanges(ctx.readConfig(file), [{
1941
+ file,
1942
+ op: "setKey",
1943
+ keyPath: ["publishConfig", "provenance"],
1944
+ value: true
1945
+ }]))
1946
+ };
1947
+ }
1948
+ } });
1843
1949
  //#endregion
1844
- //#region src/domain/rules/publish-access.ts
1950
+ //#region src/core/rules/publish-access.ts
1845
1951
  const { packageJson } = CONFIG_FILES;
1846
1952
  const publishAccessBinding = {
1847
1953
  check(ctx) {
@@ -1884,7 +1990,7 @@ const publishAccess = defineRule({
1884
1990
  title: "Declare publish access explicitly"
1885
1991
  });
1886
1992
  //#endregion
1887
- //#region src/domain/rules/store-server.ts
1993
+ //#region src/core/rules/store-server.ts
1888
1994
  const { pnpmWorkspace: pnpmWorkspace$1 } = CONFIG_FILES;
1889
1995
  const storeServer = defineRule({
1890
1996
  bindings: { pnpm: {
@@ -1901,17 +2007,17 @@ const storeServer = defineRule({
1901
2007
  state: "violation"
1902
2008
  };
1903
2009
  },
1904
- docs: "https://pnpm.io/settings#userunningStoreserver",
2010
+ docs: "https://pnpm.io/settings/store#userunningstoreserver",
1905
2011
  file: pnpmWorkspace$1
1906
2012
  } },
1907
2013
  description: "Flag use of an external store server process, which introduces a trust boundary where tampered packages could be served.",
1908
- docs: "https://pnpm.io/settings#userunningStoreserver",
2014
+ docs: "https://pnpm.io/settings/store#userunningstoreserver",
1909
2015
  id: "store-server",
1910
2016
  severity: "info",
1911
2017
  title: "Review store server usage"
1912
2018
  });
1913
2019
  //#endregion
1914
- //#region src/domain/rules/strict-allow-scripts.ts
2020
+ //#region src/core/rules/strict-allow-scripts.ts
1915
2021
  const { npmrc } = CONFIG_FILES;
1916
2022
  const strictAllowScripts = defineRule({
1917
2023
  bindings: { npm: {
@@ -1953,7 +2059,7 @@ const strictAllowScripts = defineRule({
1953
2059
  title: "Require explicit script allow-listing"
1954
2060
  });
1955
2061
  //#endregion
1956
- //#region src/domain/rules/strict-release-age.ts
2062
+ //#region src/core/rules/strict-release-age.ts
1957
2063
  const { aubeWorkspace: aubeWorkspace$2 } = CONFIG_FILES;
1958
2064
  const strictReleaseAge = withAubeParanoid(requireConfigKey({
1959
2065
  bindings: { aube: {
@@ -1970,7 +2076,7 @@ const strictReleaseAge = withAubeParanoid(requireConfigKey({
1970
2076
  title: "Enforce strict release age gate"
1971
2077
  }));
1972
2078
  //#endregion
1973
- //#region src/domain/rules/strict-store-integrity.ts
2079
+ //#region src/core/rules/strict-store-integrity.ts
1974
2080
  const { aubeWorkspace: aubeWorkspace$1 } = CONFIG_FILES;
1975
2081
  const strictStoreIntegrity = defineRule({
1976
2082
  bindings: { aube: {
@@ -2010,20 +2116,21 @@ const strictStoreIntegrity = defineRule({
2010
2116
  title: "Require tarball integrity metadata"
2011
2117
  });
2012
2118
  //#endregion
2013
- //#region src/domain/rules/trust-policy.ts
2119
+ //#region src/core/rules/trust-policy.ts
2014
2120
  const { aubeWorkspace, pnpmWorkspace } = CONFIG_FILES;
2015
2121
  const trustPolicy = withAubeParanoid(requireConfigKey({
2016
2122
  bindings: {
2017
2123
  aube: {
2018
2124
  docs: "https://aube.jdx.dev/security.html",
2019
2125
  documentedDefault: "no-downgrade",
2126
+ defaultSafety: "unconditional",
2020
2127
  file: aubeWorkspace,
2021
2128
  keyPath: ["trustPolicy"],
2022
2129
  message: "Set `trustPolicy: no-downgrade` in aube-workspace.yaml to block packages whose trust level has decreased.",
2023
2130
  value: "no-downgrade"
2024
2131
  },
2025
2132
  pnpm: {
2026
- docs: "https://pnpm.io/settings#trustpolicy",
2133
+ docs: "https://pnpm.io/settings/dependency-resolution#trustpolicy",
2027
2134
  file: pnpmWorkspace,
2028
2135
  keyPath: ["trustPolicy"],
2029
2136
  message: "Set `trustPolicy: no-downgrade` in pnpm-workspace.yaml to block packages whose trust level has decreased.",
@@ -2032,15 +2139,15 @@ const trustPolicy = withAubeParanoid(requireConfigKey({
2032
2139
  }
2033
2140
  },
2034
2141
  description: "Fail installation when a package trust level has decreased compared to previous releases, catching publisher credential downgrades.",
2035
- docs: "https://pnpm.io/settings#trustpolicy",
2142
+ docs: "https://pnpm.io/settings/dependency-resolution#trustpolicy",
2036
2143
  id: "trust-policy",
2037
2144
  severity: "warn",
2038
2145
  title: "Enforce trust policy on dependency updates"
2039
2146
  }));
2040
2147
  //#endregion
2041
- //#region src/domain/rules/unsupported-settings.ts
2042
- const bindingFor = (pm) => {
2043
- const settings = settingAvailability.filter((setting) => setting.pm === pm);
2148
+ //#region src/core/rules/unsupported-settings.ts
2149
+ const bindingFor = (pm, includesFile) => {
2150
+ const settings = settingAvailability.filter((setting) => setting.pm === pm && includesFile(setting.file.path));
2044
2151
  return { check(ctx) {
2045
2152
  const version = ctx.pmVersion;
2046
2153
  if (version === void 0) return { state: "na" };
@@ -2063,22 +2170,23 @@ const bindingFor = (pm) => {
2063
2170
  } : { state: "ok" };
2064
2171
  } };
2065
2172
  };
2066
- const unsupportedSettings = defineRule({
2173
+ const createUnsupportedSettings = (includesFile) => defineRule({
2067
2174
  id: "unsupported-settings",
2068
2175
  title: "Use settings available in the target PM version",
2069
- description: "Report configured settings whose recorded introduction version is newer than the declared or explicit stable PM target. Reports each affected file separately, grouping its unsupported keys. Workspace findings retain their member directory. Only the coverage table below is checked. Unknown targets and unlisted settings are not evaluated for availability.",
2176
+ description: "Report configured settings whose recorded introduction version is newer than the declared or explicit stable PM target. Reports each affected file separately, grouping its unsupported keys. Findings retain their evaluation directory. Only the coverage table below is checked. Unknown targets and unlisted settings are not evaluated for availability.",
2070
2177
  severity: "error",
2071
2178
  docs: "https://github.com/pHo9UBenaA/siro/blob/main/docs/rules.md#unsupported-settings--error",
2072
2179
  bindings: {
2073
- npm: bindingFor("npm"),
2074
- pnpm: bindingFor("pnpm"),
2075
- yarn: bindingFor("yarn"),
2076
- bun: bindingFor("bun"),
2077
- deno: bindingFor("deno")
2180
+ npm: bindingFor("npm", includesFile),
2181
+ pnpm: bindingFor("pnpm", includesFile),
2182
+ yarn: bindingFor("yarn", includesFile),
2183
+ bun: bindingFor("bun", includesFile),
2184
+ deno: bindingFor("deno", includesFile)
2078
2185
  }
2079
2186
  });
2187
+ const unsupportedSettings = createUnsupportedSettings(() => true);
2080
2188
  //#endregion
2081
- //#region src/domain/builtin-rules.ts
2189
+ //#region src/core/rules/builtin-rules.ts
2082
2190
  const createBuiltinRules = (time) => [
2083
2191
  advisoryCheck,
2084
2192
  approvedGitRepos,
@@ -2109,448 +2217,57 @@ const createBuiltinRules = (time) => [
2109
2217
  trustPolicy,
2110
2218
  unsupportedSettings
2111
2219
  ];
2112
- //#endregion
2113
- //#region src/composition/rules.ts
2114
- /** Read time at evaluation, never at module initialization. */
2115
- const rules = createBuiltinRules({
2116
- now: () => Date.now(),
2117
- parse: (value) => Date.parse(value)
2118
- });
2119
- //#endregion
2120
- //#region src/adapters/workspace-globs.ts
2121
- const MAX_WORKSPACE_GLOB_ALTERNATIVES = 8192;
2122
- const BRACE_EXPANSION_PROBE_LIMIT = 8193;
2123
- const boundedBraceExpand = (pattern) => {
2124
- const alternatives = braceExpand(pattern, { braceExpandMax: BRACE_EXPANSION_PROBE_LIMIT });
2125
- if (alternatives.length > MAX_WORKSPACE_GLOB_ALTERNATIVES) throw new ConfigError(`Workspace pattern expands beyond ${MAX_WORKSPACE_GLOB_ALTERNATIVES} alternatives.`);
2126
- return alternatives;
2127
- };
2128
- /** Expand with minimatch's own bounded brace semantics. */
2129
- const expandWorkspaceGlob = (pattern) => {
2220
+ const builtinScope = {
2221
+ "advisory-check": "installation",
2222
+ "approved-git-repos": "installation",
2223
+ "audit-suppression": "installation",
2224
+ "block-auto-install": "installation",
2225
+ "block-exotic-subdeps": "installation",
2226
+ "bun-security-scanner": "installation",
2227
+ "checksum-verification": "installation",
2228
+ "commit-lockfile": "installation",
2229
+ "dependency-overrides": "installation",
2230
+ "disable-lifecycle-scripts": "installation",
2231
+ "enforce-strict-ssl": "installation",
2232
+ "files-field": "manifest",
2233
+ "frozen-lockfile": "installation",
2234
+ "frozen-store": "installation",
2235
+ "hardened-mode": "installation",
2236
+ "minimum-release-age": "installation",
2237
+ "named-registries": "installation",
2238
+ "paranoid-mode": "installation",
2239
+ "patched-dependencies": "installation",
2240
+ "pin-exact-versions": "installation",
2241
+ provenance: "installation",
2242
+ "publish-access": "manifest",
2243
+ "store-server": "installation",
2244
+ "strict-allow-scripts": "installation",
2245
+ "strict-release-age": "installation",
2246
+ "strict-store-integrity": "installation",
2247
+ "trust-policy": "installation",
2248
+ "unsupported-settings": "split"
2249
+ };
2250
+ const scopeOf = (id) => Object.hasOwn(builtinScope, id) ? builtinScope[id] : "custom";
2251
+ //#endregion
2252
+ //#region src/adapters/exclusions.ts
2253
+ const compileExclusions = (patterns) => {
2130
2254
  try {
2131
- return boundedBraceExpand(pattern);
2132
- } catch (error) {
2133
- if (error instanceof TypeError) throw new ConfigError(`Invalid workspace pattern: ${error.message}.`);
2134
- throw error;
2135
- }
2136
- };
2137
- /** Engine choices live here; the application supplies only PM matching policy. */
2138
- const engineOptions = (options) => {
2139
- if (options.kind === "declaration") return {};
2140
- return {
2141
- platform: "linux",
2142
- windowsPathsNoEscape: true,
2143
- nonegate: true,
2144
- nocomment: !options.hashComments,
2145
- nocase: options.caseInsensitive,
2146
- dot: options.includeDotDirectories ?? false,
2147
- nobrace: options.syntax === "wildcards",
2148
- noext: options.syntax === "wildcards" || options.extendedPatterns === false,
2149
- optimizationLevel: options.syntax === "shell" ? 2 : 1
2150
- };
2151
- };
2152
- /** Compile once so membership, exclusions, and traversal share glob semantics. */
2153
- const compileWorkspaceGlob = (pattern, options) => {
2154
- let matcher;
2155
- try {
2156
- const settings = engineOptions(options);
2157
- if (!settings.nobrace) boundedBraceExpand(pattern);
2158
- matcher = new Minimatch(options.kind === "directory" && options.syntax === "wildcards" ? pattern.replace(/[[\]]/gu, (character) => character === "[" ? "[[]" : "[]]") : pattern, {
2159
- ...settings,
2160
- braceExpandMax: BRACE_EXPANSION_PROBE_LIMIT
2161
- });
2255
+ const matchers = patterns.map((pattern) => new Minimatch(pattern.replace(/(?:\/\*\*)+$/u, "").replace(/[\\[\]]/gu, "\\$&"), {
2256
+ dot: true,
2257
+ nocase: false,
2258
+ nonegate: true,
2259
+ nocomment: true,
2260
+ nobrace: true,
2261
+ noext: true,
2262
+ platform: "linux"
2263
+ }));
2264
+ return (directory) => matchers.some((matcher) => matcher.match(directory));
2162
2265
  } catch (error) {
2163
- if (error instanceof TypeError) throw new ConfigError(`Invalid workspace pattern: ${error.message}.`);
2164
- throw error;
2165
- }
2166
- return {
2167
- matches: (directory) => matcher.match(directory),
2168
- canDescend(directory) {
2169
- const parts = directory.split("/");
2170
- return matcher.set.some((alternative) => (alternative.includes(GLOBSTAR) || parts.length < alternative.length) && matcher.matchOne(parts, alternative, true));
2171
- }
2172
- };
2173
- };
2174
- const minimatchGlobs = {
2175
- expand: expandWorkspaceGlob,
2176
- compile: compileWorkspaceGlob
2177
- };
2178
- //#endregion
2179
- //#region src/domain/services/parse-config-file.ts
2180
- const EMPTY_FILE = Object.freeze({});
2181
- /** Create a fresh parser and cache for one lint command. */
2182
- const createConfigParser = (codecFor, ctx) => {
2183
- const cache = /* @__PURE__ */ new Map();
2184
- return (file) => {
2185
- if (file === void 0) return EMPTY_FILE;
2186
- const cacheKey = `${file.kind}:${file.path}`;
2187
- const cached = cache.get(cacheKey);
2188
- if (cached !== void 0) return cached;
2189
- const text = ctx.readText(file.path);
2190
- if (text === void 0) {
2191
- cache.set(cacheKey, EMPTY_FILE);
2192
- return EMPTY_FILE;
2193
- }
2194
- const parsed = wrapCodecError(file.path, () => codecFor(file.kind).parse(text));
2195
- cache.set(cacheKey, parsed);
2196
- return parsed;
2197
- };
2198
- };
2199
- //#endregion
2200
- //#region src/application/workspace-pattern.ts
2201
- /** Detect the basic * and ? markers shared across workspace dialects. */
2202
- const hasBasicWorkspaceWildcard = (pattern) => pattern.includes("*") || pattern.includes("?");
2203
- /** Workspace declarations use POSIX separators regardless of the host platform. */
2204
- const stripTrailingWorkspaceSlashes = (pattern) => {
2205
- let end = pattern.length;
2206
- while (end > 0 && pattern[end - 1] === "/") end -= 1;
2207
- return pattern.slice(0, end);
2208
- };
2209
- //#endregion
2210
- //#region src/application/workspace-prefix.ts
2211
- /** Anchor Deno's native literal prefix before applying its glob suffix. */
2212
- const anchorWorkspacePrefix = (pattern, glob, resolveChild) => {
2213
- const parts = pattern.split("/");
2214
- const firstWildcard = parts.findIndex(hasBasicWorkspaceWildcard);
2215
- const prefix = parts.slice(0, firstWildcard < 0 ? parts.length : firstWildcard);
2216
- if (prefix.length === 0 || pattern === ".") return glob;
2217
- const declared = prefix.join("/");
2218
- let resolved;
2219
- let attempted = false;
2220
- const resolve = () => {
2221
- if (!attempted) {
2222
- attempted = true;
2223
- let current = ".";
2224
- for (const part of prefix) {
2225
- const child = resolveChild(current, part);
2226
- if (child === void 0) return void 0;
2227
- current = current === "." ? child : `${current}/${child}`;
2228
- }
2229
- resolved = current;
2230
- }
2231
- return resolved;
2232
- };
2233
- const rewrite = (directory, actual) => directory === actual ? declared : directory.startsWith(`${actual}/`) ? declared + directory.slice(actual.length) : void 0;
2234
- return {
2235
- matches(directory) {
2236
- if (directory === ".") return false;
2237
- const actual = resolve();
2238
- const rewritten = actual === void 0 ? void 0 : rewrite(directory, actual);
2239
- return rewritten !== void 0 && glob.matches(rewritten);
2240
- },
2241
- canDescend(directory) {
2242
- const actual = resolve();
2243
- if (actual === void 0) return false;
2244
- if (directory === "." || actual.startsWith(`${directory}/`)) return true;
2245
- const rewritten = rewrite(directory, actual);
2246
- return rewritten !== void 0 && glob.canDescend(rewritten);
2247
- }
2248
- };
2249
- };
2250
- //#endregion
2251
- //#region src/application/workspace-glob-policy.ts
2252
- const workspaceGlobOptions = (caseInsensitive) => ({
2253
- kind: "directory",
2254
- syntax: "shell",
2255
- caseInsensitive
2256
- });
2257
- //#endregion
2258
- //#region src/application/workspace-dialects.ts
2259
- const containsUnsupportedAubeGlobSyntax = (pattern) => /[[\]{}()]/u.test(pattern);
2260
- const matchesCrossPathGlob = (pattern, value) => {
2261
- const patternCharacters = [...pattern];
2262
- const valueCharacters = [...value];
2263
- let patternIndex = 0;
2264
- let valueIndex = 0;
2265
- let starIndex = -1;
2266
- let retryIndex = 0;
2267
- while (valueIndex < valueCharacters.length) {
2268
- const character = patternCharacters[patternIndex];
2269
- if (character === "?" || character === valueCharacters[valueIndex]) {
2270
- patternIndex += 1;
2271
- valueIndex += 1;
2272
- } else if (character === "*") {
2273
- starIndex = patternIndex;
2274
- retryIndex = valueIndex;
2275
- patternIndex += 1;
2276
- } else if (starIndex >= 0) {
2277
- patternIndex = starIndex + 1;
2278
- retryIndex += 1;
2279
- valueIndex = retryIndex;
2280
- } else return false;
2281
- }
2282
- while (patternCharacters[patternIndex] === "*") patternIndex += 1;
2283
- return patternIndex === patternCharacters.length;
2284
- };
2285
- /** The bounded glob subset verified against the recorded Deno/Aube sources. */
2286
- const compileAdditionalWorkspaceGlob = (pattern, pm, excluded, caseInsensitive, globs) => {
2287
- const parts = pattern.split("/");
2288
- if (parts.some((part) => part.includes("**") && part !== "**")) throw new ConfigError(`${pm}: ** must occupy an entire workspace path component.`);
2289
- if (pm === "aube" && (containsUnsupportedAubeGlobSyntax(pattern) || pattern.includes("**") && (parts.at(-1) !== "**" || hasBasicWorkspaceWildcard(parts.slice(0, -1).join("/"))))) throw new ConfigError("Aube workspace inspection supports literals, * and ?, and a trailing ** after a literal prefix; other glob forms are not yet supported.");
2290
- const matcher = globs.compile(pattern, {
2291
- kind: "directory",
2292
- syntax: "wildcards",
2293
- includeDotDirectories: pm === "aube",
2294
- caseInsensitive: pm === "deno" && (hasBasicWorkspaceWildcard(pattern) || caseInsensitive)
2295
- });
2296
- if (pm === "deno" && hasBasicWorkspaceWildcard(pattern)) {
2297
- const files = globs.compile(`${pattern}/package.json`, {
2298
- kind: "directory",
2299
- syntax: "wildcards",
2300
- caseInsensitive: true
2301
- });
2302
- return {
2303
- ...matcher,
2304
- matches: (directory) => files.matches(directory === "." ? "package.json" : `${directory}/package.json`)
2305
- };
2306
- }
2307
- if (pm !== "aube" || !excluded) return matcher;
2308
- const self = pattern.endsWith("/**") ? pattern.slice(0, -3) : void 0;
2309
- return {
2310
- ...matcher,
2311
- matches: (directory) => matchesCrossPathGlob(pattern, directory) || directory === self
2312
- };
2313
- };
2314
- //#endregion
2315
- //#region src/application/workspaces.ts
2316
- /** npm cancels an earlier exclusion when a later positive pattern matches it. */
2317
- const splitNpmPattern = (raw) => {
2318
- const prefix = /^!+/u.exec(raw)?.[0] ?? "";
2319
- return {
2320
- excluded: prefix.length % 2 === 1,
2321
- pattern: raw.slice(prefix.length)
2322
- };
2323
- };
2324
- const npmPatterns = (patterns, globs) => {
2325
- const positive = [];
2326
- let negative = [];
2327
- for (const raw of patterns) {
2328
- const parsed = splitNpmPattern(raw);
2329
- const pattern = parsed.pattern.replace(/^\.?\/+/u, "");
2330
- if (parsed.excluded) negative.push({
2331
- pattern,
2332
- glob: globs.compile(pattern, { kind: "declaration" })
2333
- });
2334
- else {
2335
- for (let index = 0; index < negative.length; index += 1) if (negative[index]?.glob.matches(pattern)) negative.splice(index, 1);
2336
- positive.push(pattern);
2337
- }
2338
- }
2339
- return [...positive, ...negative.map(({ pattern }) => `!${pattern}`)];
2340
- };
2341
- /** Read declaration sources separately so Deno's two manifest sets stay distinct. */
2342
- const workspaceDefinitions = (ctx, pm, dependencies) => {
2343
- const { codecFor, paths, globs } = dependencies;
2344
- const parse = createConfigParser(codecFor, ctx);
2345
- const validate = (value, source, denoManifests = false) => {
2346
- if (value === void 0) return {
2347
- patterns: [],
2348
- denoManifests
2349
- };
2350
- if (!Array.isArray(value) || !Array.from(value).every((item) => typeof item === "string")) throw new ConfigError(`${source}: expected an array of directory patterns.`);
2351
- for (const pattern of value) {
2352
- const positive = pm === "npm" ? splitNpmPattern(pattern).pattern : pattern.startsWith("!") ? pattern.slice(1) : pattern;
2353
- if ((pm === "deno" || pm === "aube" ? [positive] : globs.expand(positive)).some((alternative) => !isRelPath(alternative) || alternative.includes("\\") || alternative.startsWith("!"))) throw new ConfigError(`${source}: use relative directory patterns without parent traversal: ${JSON.stringify(pattern)}.`);
2354
- if (pm === "deno" && denoManifests && !pattern.startsWith("!") && paths.normalizePattern(positive) === ".") throw new ConfigError(`${source}: a Deno workspace cannot contain itself.`);
2355
- }
2356
- return {
2357
- patterns: pm === "npm" ? npmPatterns(value, globs) : value,
2358
- denoManifests
2359
- };
2360
- };
2361
- const packageDefinition = () => {
2362
- let value = ctx.packageJson?.workspaces;
2363
- if (isPlainRecord(value)) {
2364
- value = value.packages;
2365
- if (value === void 0) throw new ConfigError("package.json#workspaces: expected a packages array.");
2366
- }
2367
- return validate(value, "package.json#workspaces");
2368
- };
2369
- if (pm === "pnpm") return [validate(ctx.exists(CONFIG_FILES.pnpmWorkspace.path) ? parse(CONFIG_FILES.pnpmWorkspace).packages : void 0, "pnpm-workspace.yaml#packages")];
2370
- if (pm === "aube") {
2371
- const file = [CONFIG_FILES.aubeWorkspace, CONFIG_FILES.pnpmWorkspace].find((candidate) => ctx.exists(candidate.path));
2372
- return [file ? validate(parse(file).packages, `${file.path}#packages`) : packageDefinition()];
2373
- }
2374
- if (pm === "deno") return [validate(parse(CONFIG_FILES.denoJson).workspace, "deno.json#workspace", true), packageDefinition()];
2375
- return [packageDefinition()];
2376
- };
2377
- /** Traverse only ordinary directories that can fall under a positive pattern. */
2378
- const workspaceDirectories = (ctx, fs, definition, pm, dependencies) => {
2379
- const { codecFor, paths, globs, caseInsensitiveGlobs } = dependencies;
2380
- const defaultWorkspaceGlobOptions = workspaceGlobOptions(caseInsensitiveGlobs);
2381
- const { patterns } = definition;
2382
- const skipVendor = pm === "deno" && createConfigParser(codecFor, ctx)(CONFIG_FILES.denoJson).vendor === true;
2383
- const positive = patterns.filter((pattern) => !pattern.startsWith("!")).map((pattern) => stripTrailingWorkspaceSlashes(paths.normalizePattern(pattern)));
2384
- const negative = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => stripTrailingWorkspaceSlashes(paths.normalizePattern(pattern.slice(1))));
2385
- const directoryCache = /* @__PURE__ */ new Map();
2386
- const readDirectories = (directory) => {
2387
- const cached = directoryCache.get(directory);
2388
- if (cached) return cached;
2389
- if (!fs.readDirectories) throw new UsageError("Workspace discovery requires FileSystem.readDirectories; no host filesystem fallback is used.");
2390
- const names = fs.readDirectories(paths.resolve(ctx.root, asRelPath(directory)));
2391
- if (!Array.isArray(names)) throw new ConfigError("FileSystem.readDirectories must return an array of directory names.");
2392
- for (const name of Array.from(names)) if (!isRelPath(name) || name === "." || name === ".." || /[\\/]/u.test(name)) throw new ConfigError("FileSystem.readDirectories returned an invalid child directory name.");
2393
- const result = [...new Set(names)].sort();
2394
- directoryCache.set(directory, result);
2395
- return result;
2396
- };
2397
- const resolveChild = (parent, name) => {
2398
- if (name === ".git" || name === "node_modules") return void 0;
2399
- const names = readDirectories(parent);
2400
- const resolved = names.includes(name) ? name : fs.resolveDirectory?.(paths.resolve(ctx.root, asRelPath(parent)), name);
2401
- if (resolved !== void 0 && !names.includes(resolved)) throw new ConfigError("FileSystem.resolveDirectory must return an enumerated ordinary child directory name.");
2402
- return resolved === ".git" || resolved === "node_modules" ? void 0 : resolved;
2403
- };
2404
- const compile = (pattern, excluded = false) => {
2405
- if (pm !== "deno" && pm !== "aube") return globs.compile(pattern, pm === "npm" ? {
2406
- ...defaultWorkspaceGlobOptions,
2407
- hashComments: true
2408
- } : defaultWorkspaceGlobOptions);
2409
- const glob = compileAdditionalWorkspaceGlob(pattern, pm, excluded, caseInsensitiveGlobs, globs);
2410
- return pm === "deno" && !excluded ? anchorWorkspacePrefix(pattern, glob, resolveChild) : glob;
2411
- };
2412
- const included = positive.map((pattern) => compile(pattern));
2413
- const excluded = negative.map((pattern) => {
2414
- return {
2415
- glob: compile(pattern, true),
2416
- subtree: pm === "deno" || pm === "aube" || pm === "bun" ? void 0 : pattern === "**" ? { matches: () => true } : pattern.endsWith("/**") ? compile(pattern.slice(0, -3), true) : void 0
2417
- };
2418
- });
2419
- const declaredBase = (pattern) => {
2420
- const parts = pattern.split("/");
2421
- const wildcard = parts.findIndex(hasBasicWorkspaceWildcard);
2422
- return parts.slice(0, wildcard < 0 ? parts.length : wildcard).join("/") || ".";
2423
- };
2424
- const relatedBases = (left, right) => left === "." || right === "." || left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
2425
- const denoPositive = pm === "deno" ? positive.filter(hasBasicWorkspaceWildcard).map((pattern) => ({
2426
- base: declaredBase(pattern),
2427
- scope: anchorWorkspacePrefix(declaredBase(pattern), {
2428
- matches: () => true,
2429
- canDescend: () => true
2430
- }, resolveChild)
2431
- })) : [];
2432
- const denoOrdered = pm === "deno" ? patterns.filter((pattern) => hasBasicWorkspaceWildcard(pattern) || pattern.startsWith("!")).map((raw) => {
2433
- const isExcluded = raw.startsWith("!");
2434
- const pattern = stripTrailingWorkspaceSlashes(paths.normalizePattern(isExcluded ? raw.slice(1) : raw));
2435
- const glob = compile(pattern, isExcluded);
2436
- if (!isExcluded) return {
2437
- excluded: false,
2438
- glob
2439
- };
2440
- return {
2441
- excluded: true,
2442
- glob: {
2443
- ...glob,
2444
- matches(directory) {
2445
- if (!hasBasicWorkspaceWildcard(pattern)) return directory === pattern;
2446
- if (!glob.matches(directory)) return false;
2447
- const applicable = denoPositive.filter((item) => item.scope.matches(directory)).map((item) => relatedBases(item.base, declaredBase(pattern)));
2448
- if (applicable.some(Boolean) && applicable.some((value) => !value)) throw new ConfigError("Deno workspace exclusion has ambiguous overlapping bases; use consistent prefix spelling or separate non-overlapping patterns.");
2449
- return applicable.some(Boolean);
2450
- }
2451
- }
2452
- };
2453
- }) : [];
2454
- const bunPatterns = pm === "bun" ? patterns.map((raw) => {
2455
- const isExcluded = raw.startsWith("!");
2456
- const pattern = stripTrailingWorkspaceSlashes(paths.normalizePattern(isExcluded ? raw.slice(1) : raw));
2457
- const usesGlob = isExcluded || hasBasicWorkspaceWildcard(pattern) || pattern.includes("{") || pattern.includes("[");
2458
- const glob = globs.compile(pattern, {
2459
- ...defaultWorkspaceGlobOptions,
2460
- extendedPatterns: false
2461
- });
2462
- const subtree = isExcluded && pattern.endsWith("/**") ? globs.compile(pattern.slice(0, -3), {
2463
- ...defaultWorkspaceGlobOptions,
2464
- extendedPatterns: false
2465
- }) : void 0;
2466
- return {
2467
- isExcluded,
2468
- usesGlob,
2469
- glob,
2470
- matches: (directory) => glob.matches(directory) || glob.matches(`${directory}/`) || subtree?.matches(directory) === true
2471
- };
2472
- }) : [];
2473
- const bunOrdered = bunPatterns.filter(({ usesGlob }) => usesGlob);
2474
- const bunLiterals = bunPatterns.filter(({ isExcluded, usesGlob }) => !isExcluded && !usesGlob);
2475
- const bunTraversal = bunPatterns.filter(({ isExcluded }) => !isExcluded).map(({ glob }) => glob);
2476
- const denoLiteral = pm === "deno" ? positive.filter((pattern) => !hasBasicWorkspaceWildcard(pattern)).map((pattern) => compile(pattern)) : [];
2477
- if (definition.denoManifests && denoOrdered.findLast(({ glob }) => glob.matches("."))?.excluded === false) throw new ConfigError("deno.json#workspace: a Deno workspace cannot contain itself.");
2478
- if (positive.length === 0 || positive.every((pattern) => pattern === ".")) return [];
2479
- if (!fs.readDirectories) throw new UsageError("Workspace discovery requires FileSystem.readDirectories; no host filesystem fallback is used.");
2480
- const result = [];
2481
- const pending = ["."];
2482
- while (pending.length > 0) {
2483
- const current = pending.pop();
2484
- if (current === void 0) break;
2485
- for (const name of readDirectories(current)) {
2486
- if (name === ".git" || name === "node_modules") continue;
2487
- const directory = asRelPath(current === "." ? name : `${current}/${name}`);
2488
- if (pm === "bun" && name === "CMakeFiles" && !bunLiterals.some(({ glob }) => glob.matches(directory) || glob.canDescend(directory))) continue;
2489
- const inVendor = skipVendor && (directory === "vendor" || directory.startsWith("vendor/"));
2490
- if (inVendor && !denoLiteral.some((pattern) => pattern.matches(directory) || pattern.canDescend(directory))) continue;
2491
- const isExcluded = pm !== "deno" && excluded.some(({ glob }) => glob.matches(directory) || glob.matches(`${directory}/`));
2492
- if (excluded.some(({ subtree }) => subtree?.matches(directory))) continue;
2493
- const lastDenoMatch = denoOrdered.findLast(({ glob }) => glob.matches(directory));
2494
- const lastBunMatch = bunOrdered.findLast(({ matches }) => matches(directory));
2495
- if (pm === "deno" ? denoLiteral.some((pattern) => pattern.matches(directory)) || !inVendor && lastDenoMatch?.excluded === false : pm === "bun" ? bunLiterals.some(({ glob }) => glob.matches(directory)) || lastBunMatch?.isExcluded === false : !isExcluded && included.some((pattern) => pattern.matches(directory))) result.push(directory);
2496
- if ((pm === "bun" ? bunTraversal : inVendor ? denoLiteral : included).some((pattern) => pattern.canDescend(directory))) pending.push(directory);
2497
- }
2266
+ throw new ConfigError(`exclude: ${error instanceof Error ? error.message : String(error)}`);
2498
2267
  }
2499
- return result.sort();
2500
- };
2501
- //#endregion
2502
- //#region src/application/workspace-contexts.ts
2503
- /** Expand each manager's declarations into isolated publication contexts. */
2504
- const collectWorkspaceMembers = (ctx, fs, pms, projectType, dependencies) => {
2505
- const { codecFor, createRepoContext, paths } = dependencies;
2506
- return pms.flatMap((pm) => {
2507
- const seen = /* @__PURE__ */ new Set();
2508
- return workspaceDefinitions(ctx, pm, dependencies).flatMap((definition) => workspaceDirectories(ctx, fs, definition, pm, dependencies).flatMap((directory) => {
2509
- const root = paths.resolve(ctx.root, directory);
2510
- const manifest = paths.resolve(root, asRelPath("package.json"));
2511
- const denoManifest = paths.resolve(root, asRelPath("deno.json"));
2512
- const denoJsonc = paths.resolve(root, asRelPath("deno.jsonc"));
2513
- const allowed = new Set(pm === "deno" ? [
2514
- manifest,
2515
- denoManifest,
2516
- denoJsonc
2517
- ] : [manifest]);
2518
- const memberFs = {
2519
- exists: (file) => allowed.has(file) && fs.exists(file),
2520
- readText: (file) => allowed.has(file) ? fs.readText(file) : void 0
2521
- };
2522
- try {
2523
- const explicitMember = definition.patterns.some((pattern) => !pattern.startsWith("!") && !hasBasicWorkspaceWildcard(pattern) && stripTrailingWorkspaceSlashes(paths.normalizePattern(pattern)) === directory);
2524
- if (!definition.denoManifests && !fs.exists(manifest)) {
2525
- if (pm === "deno" && explicitMember) throw new ConfigError("Declared npm workspace member has no package.json.");
2526
- return [];
2527
- }
2528
- if (pm === "deno" && !fs.exists(denoManifest) && fs.exists(denoJsonc)) throw new ConfigError("deno.jsonc is not supported; use strict deno.json.");
2529
- const memberCtx = createRepoContext(root, memberFs, projectType);
2530
- if (!definition.denoManifests && !memberCtx.packageJson) return [];
2531
- if (!memberCtx.packageJson && !memberCtx.exists(asRelPath("deno.json"))) {
2532
- if (definition.denoManifests && explicitMember) throw new ConfigError("Declared Deno member has no deno.json or package.json.");
2533
- return [];
2534
- }
2535
- if (pm === "deno") {
2536
- if (createConfigParser(codecFor, memberCtx)(CONFIG_FILES.denoJson).workspace !== void 0 || memberCtx.packageJson?.workspaces !== void 0) throw new ConfigError("Nested workspace declarations are not supported by Deno.");
2537
- }
2538
- if (seen.has(directory)) return [];
2539
- seen.add(directory);
2540
- return [{
2541
- directory,
2542
- pm,
2543
- ctx: memberCtx
2544
- }];
2545
- } catch (error) {
2546
- if (error instanceof ConfigError) throw new ConfigError(`${directory}/${error.message}`);
2547
- throw error;
2548
- }
2549
- }));
2550
- });
2551
2268
  };
2552
2269
  //#endregion
2553
- //#region src/domain/services/apply-config.ts
2270
+ //#region src/core/apply-config.ts
2554
2271
  /** Select active rules and keep user severity choices as explicit runtime data. */
2555
2272
  const applyConfig = (base, config) => {
2556
2273
  const merged = [...base, ...config?.customRules ?? []];
@@ -2578,7 +2295,7 @@ const applyConfig = (base, config) => {
2578
2295
  };
2579
2296
  };
2580
2297
  //#endregion
2581
- //#region src/domain/services/detect-pms.ts
2298
+ //#region src/core/detect-pms.ts
2582
2299
  const registerSignals = (signals, pm, owner) => {
2583
2300
  for (const file of signals) {
2584
2301
  const prior = owner.get(file);
@@ -2610,24 +2327,25 @@ const detectPMs = (ctx) => {
2610
2327
  return PMS.filter((pm) => found.has(pm));
2611
2328
  };
2612
2329
  //#endregion
2613
- //#region src/domain/services/resolve-pms.ts
2330
+ //#region src/core/resolve-pms.ts
2614
2331
  /** Apply the explicit selection or detection, then the configured allow-list. */
2615
2332
  const resolvePMs = (ctx, opts) => {
2616
2333
  const { pmOverride, allowed } = opts;
2617
2334
  const detected = pmOverride ? [pmOverride] : detectPMs(ctx);
2618
2335
  const pms = allowed ? detected.filter((pm) => allowed.includes(pm)) : detected;
2619
2336
  if (pms.length > 0) return pms;
2337
+ if (opts.optional && detected.length === 0) return [];
2620
2338
  if (!allowed) throw new UsageError(`No package manager detected. Pass --pm <${PMS.join("|")}> to be explicit.`);
2621
2339
  if (pmOverride) throw new UsageError(`--pm ${pmOverride} is not in siro.config.ts pms (${allowed.join(", ")}). Adjust the config or the flag.`);
2622
2340
  if (detected.length > 0) throw new UsageError(`Detected PMs (${detected.join(", ")}) do not match siro.config.ts pms (${allowed.join(", ")}). Adjust the config or remove the restriction.`);
2623
2341
  throw new UsageError(`No package manager detected, and siro.config.ts restricts pms to ${allowed.join(", ")}. Adjust the config or remove the restriction.`);
2624
2342
  };
2625
2343
  //#endregion
2626
- //#region src/domain/services/decide-severity.ts
2344
+ //#region src/core/decide-severity.ts
2627
2345
  /** Resolve the most specific severity signal for one finding. */
2628
2346
  const decideSeverity = (status, binding, rule, userOverride) => userOverride ?? status.severity ?? binding.severity ?? rule.severity;
2629
2347
  //#endregion
2630
- //#region src/domain/services/render-version-note.ts
2348
+ //#region src/core/render-version-note.ts
2631
2349
  const buildVersionNoteParts = (versionNote) => {
2632
2350
  const parts = [];
2633
2351
  if (typeof versionNote.configAvailableSince !== "undefined") parts.push(`available since ${versionNote.configAvailableSince}`);
@@ -2642,130 +2360,342 @@ const renderVersionNoteMessage = (message, versionNote) => {
2642
2360
  return `${message} (${parts.join("; ")})`;
2643
2361
  };
2644
2362
  //#endregion
2645
- //#region src/application/run-lint.ts
2363
+ //#region src/core/evaluate-binding.ts
2364
+ /** One response boundary for generic and PM-specific checks. Selection stays with callers. */
2365
+ const evaluateBinding = (repository, rule, binding, overrides, target) => {
2366
+ const response = binding.check({
2367
+ ...repository.ctx,
2368
+ readConfig: repository.parseConfig,
2369
+ pmVersion: target?.version
2370
+ }, repository.parseConfig(binding.file));
2371
+ assertSynchronous(response, `Rule '${rule.id}' check`);
2372
+ if (!isCheckStatusShape(response)) throw new ConfigError(`Rule '${rule.id}' returned an invalid check result.`);
2373
+ return (response.state === "violations" ? response.violations : [response]).flatMap((status) => status.state !== "violation" ? [] : [{
2374
+ ruleId: rule.id,
2375
+ directory: ".",
2376
+ ...target ? { pm: target.pm } : {},
2377
+ severity: decideSeverity(status, binding, rule, overrides?.get(rule.id)),
2378
+ message: renderVersionNoteMessage(status.message, binding.versionNote),
2379
+ file: status.file ?? binding.file?.path,
2380
+ docs: binding.docs ?? rule.docs,
2381
+ actual: status.actual,
2382
+ expected: status.expected,
2383
+ remediation: target ? guardRemediationAvailability(target.pm, target.version, status.remediation) : status.remediation
2384
+ }]);
2385
+ };
2386
+ //#endregion
2387
+ //#region src/core/run-lint.ts
2646
2388
  const resolveBindingProjectType = (ctx, pm, parseConfig) => {
2647
2389
  if (ctx.projectType !== void 0) return ctx.projectType;
2648
- if (pm === "deno") return resolveDenoProjectType(ctx, parseConfig(CONFIG_FILES.denoJson));
2649
- return resolvePackageJsonProjectType(ctx);
2390
+ return pm === "deno" ? resolveDenoProjectType(ctx, parseConfig(CONFIG_FILES.denoJson)) : resolvePackageJsonProjectType(ctx);
2650
2391
  };
2651
- /** Evaluate every applicable rule binding and collect violations. */
2392
+ /** Select applicable PM bindings; response handling is shared with manifest checks. */
2652
2393
  const runLint = (opts) => {
2653
- const { ctx, pms, ruleSet, severityOverrides, codecFor } = opts;
2394
+ const { repository, pms, ruleSet, severityOverrides } = opts;
2654
2395
  const findings = [];
2396
+ for (const rule of ruleSet) for (const pm of pms) {
2397
+ const binding = rule.bindings[pm];
2398
+ if (!binding || rule.projectTypes && !rule.projectTypes.includes(resolveBindingProjectType(repository.ctx, pm, repository.parseConfig))) continue;
2399
+ findings.push(...evaluateBinding(repository, rule, binding, severityOverrides, {
2400
+ pm,
2401
+ version: opts.pmVersions?.[pm]
2402
+ }));
2403
+ }
2655
2404
  const summary = {
2656
2405
  error: 0,
2657
2406
  info: 0,
2658
2407
  warn: 0
2659
2408
  };
2660
- const parseConfig = createConfigParser(codecFor, ctx);
2661
- for (const rule of ruleSet) for (const pm of pms) {
2662
- const binding = rule.bindings[pm];
2663
- if (!binding || rule.projectTypes && !rule.projectTypes.includes(resolveBindingProjectType(ctx, pm, parseConfig))) continue;
2664
- const ruleContext = {
2665
- ...ctx,
2666
- readConfig: parseConfig,
2667
- pmVersion: opts.pmVersions?.[pm]
2668
- };
2669
- const response = binding.check(ruleContext, parseConfig(binding.file));
2670
- if (!isCheckStatusShape(response)) throw new ConfigError(`Rule '${rule.id}' returned an invalid check result.`);
2671
- const statuses = response.state === "violations" ? response.violations : [response];
2672
- for (const status of statuses) {
2673
- if (status.state !== "violation") continue;
2674
- const finding = {
2675
- ruleId: rule.id,
2676
- pm,
2677
- severity: decideSeverity(status, binding, rule, severityOverrides?.get(rule.id)),
2678
- message: renderVersionNoteMessage(status.message, binding.versionNote),
2679
- file: status.file ?? binding.file?.path,
2680
- docs: binding.docs ?? rule.docs,
2681
- actual: status.actual,
2682
- expected: status.expected,
2683
- remediation: guardRemediationAvailability(pm, ruleContext.pmVersion, status.remediation)
2684
- };
2685
- findings.push(finding);
2686
- summary[finding.severity] += 1;
2687
- }
2688
- }
2409
+ for (const finding of findings) summary[finding.severity] += 1;
2689
2410
  return {
2690
2411
  findings,
2691
2412
  summary
2692
2413
  };
2693
2414
  };
2694
2415
  //#endregion
2695
- //#region src/application/lint.ts
2696
- /** Evaluate a repository without reporting or executing configuration files. */
2697
- const prepareLint = (options, dependencies) => {
2698
- const { paths, codecFor, createRepoContext } = dependencies;
2416
+ //#region src/core/contracts/deno-json.ts
2417
+ /** Validate consumed publication metadata, not Deno's entire versioned schema. */
2418
+ const validateDenoMetadata = (config) => {
2419
+ const name = getByPath(config, ["name"]);
2420
+ if (name != null && typeof name !== "string") throw new ConfigError("deno.json: name must be a string or null.");
2421
+ const publish = getByPath(config, ["publish"]);
2422
+ if (publish == null || typeof publish === "boolean") return;
2423
+ if (!isPlainRecord(publish)) throw new ConfigError("deno.json: publish must be a boolean, object or null.");
2424
+ const include = getByPath(publish, ["include"]);
2425
+ if (include != null && (!Array.isArray(include) || !Array.from(include).every((item) => typeof item === "string"))) throw new ConfigError("deno.json: publish.include must be a string array or null.");
2426
+ };
2427
+ //#endregion
2428
+ //#region src/core/parse-config-file.ts
2429
+ const EMPTY_FILE = Object.freeze({});
2430
+ /** Create a lazy parser for one repository context; callers own its lifetime. */
2431
+ const createConfigParser = (codecFor, ctx) => {
2432
+ const cache = /* @__PURE__ */ new Map();
2433
+ return (file) => {
2434
+ if (file === void 0) return EMPTY_FILE;
2435
+ const cacheKey = `${file.kind}:${file.path}`;
2436
+ const cached = cache.get(cacheKey);
2437
+ if (cached !== void 0) return cached;
2438
+ const text = ctx.readText(file.path);
2439
+ if (text === void 0) {
2440
+ cache.set(cacheKey, EMPTY_FILE);
2441
+ return EMPTY_FILE;
2442
+ }
2443
+ const parsed = wrapCodecError(file.path, () => codecFor(file.kind).parse(text));
2444
+ cache.set(cacheKey, parsed);
2445
+ return parsed;
2446
+ };
2447
+ };
2448
+ /** Pair the context with a fresh lazy parser; construction performs no reads. */
2449
+ const createRepositoryEvaluation = (ctx, codecFor) => ({
2450
+ ctx,
2451
+ parseConfig: createConfigParser(codecFor, ctx)
2452
+ });
2453
+ //#endregion
2454
+ //#region src/core/discovery.ts
2455
+ /** One context per selected directory, shared by publication and installation passes. */
2456
+ const discover = (cwd, fs, excluded, projectType, dependencies) => {
2457
+ if (typeof fs.readDirectories !== "function") throw new ConfigError("FileSystem.readDirectories is required for package discovery; no native filesystem fallback is used.");
2458
+ const { paths, createRepoContext, codecFor } = dependencies;
2459
+ const pending = [asRelPath(".")];
2460
+ const directories = [];
2461
+ while (pending.length) {
2462
+ const directory = pending.pop();
2463
+ const absolute = paths.resolve(cwd, directory);
2464
+ let repository;
2465
+ const manifests = [];
2466
+ try {
2467
+ repository = createRepositoryEvaluation(createRepoContext(absolute, fs, projectType), codecFor);
2468
+ const { ctx, parseConfig } = repository;
2469
+ if (ctx.packageJson !== void 0) manifests.push("package.json");
2470
+ if (ctx.readText(CONFIG_FILES.denoJson.path) !== void 0) {
2471
+ validateDenoMetadata(parseConfig(CONFIG_FILES.denoJson));
2472
+ manifests.push("deno.json");
2473
+ } else if (ctx.exists(asRelPath("deno.jsonc"))) throw new ConfigError("deno.jsonc is not supported; use strict deno.json.");
2474
+ } catch (error) {
2475
+ if (error instanceof ConfigError && directory !== ".") throw new ConfigError(`${directory}/${error.message}`);
2476
+ throw error;
2477
+ }
2478
+ directories.push({
2479
+ directory,
2480
+ repository,
2481
+ manifests
2482
+ });
2483
+ const names = fs.readDirectories(absolute);
2484
+ if (!Array.isArray(names)) throw new ConfigError(`${directory}: FileSystem.readDirectories must return a dense array.`);
2485
+ const children = /* @__PURE__ */ new Set();
2486
+ for (let i = 0; i < names.length; i += 1) {
2487
+ if (!Object.hasOwn(names, i)) throw new ConfigError(`${directory}: FileSystem.readDirectories must return a dense array.`);
2488
+ const child = paths.child(directory, names[i]);
2489
+ if (names[i] === ".git" || names[i] === "node_modules" || excluded(child) || children.has(child)) continue;
2490
+ children.add(child);
2491
+ pending.push(child);
2492
+ }
2493
+ }
2494
+ return directories.sort((a, b) => a.directory < b.directory ? -1 : a.directory > b.directory ? 1 : 0);
2495
+ };
2496
+ //#endregion
2497
+ //#region src/core/manifest-checks.ts
2498
+ const manifestProjectType = (repository, file) => file === "deno.json" ? resolveDenoProjectType(repository.ctx, repository.parseConfig(CONFIG_FILES.denoJson)) : resolvePackageJsonProjectType(repository.ctx);
2499
+ const selectBinding = {
2500
+ "files-field": (file) => file === "deno.json" ? denoPublishBinding : packageJsonFilesBinding,
2501
+ "publish-access": (file, repository, targets) => {
2502
+ if (file !== "package.json") return void 0;
2503
+ return repository.ctx.packageJson?.publishConfig?.access === "private" && targets.length > 0 ? "pm" : publishAccessBinding;
2504
+ }
2505
+ };
2506
+ const checkManifest = (repository, file, targets, rules, overrides) => {
2507
+ const findings = [];
2508
+ const projectType = manifestProjectType(repository, file);
2509
+ const pms = targets.map((target) => target.pm);
2510
+ const pmVersions = Object.fromEntries(targets.map((target) => [target.pm, target.version]));
2511
+ for (const rule of rules) {
2512
+ if (rule.projectTypes && !rule.projectTypes.includes(projectType)) continue;
2513
+ if (rule.id === "unsupported-settings") findings.push(...runLint({
2514
+ repository,
2515
+ pms,
2516
+ pmVersions,
2517
+ ruleSet: [{
2518
+ ...rule,
2519
+ bindings: createUnsupportedSettings((path) => path === file).bindings
2520
+ }],
2521
+ severityOverrides: overrides
2522
+ }).findings);
2523
+ else if (Object.hasOwn(selectBinding, rule.id)) {
2524
+ const binding = selectBinding[rule.id](file, repository, targets);
2525
+ if (binding === "pm") findings.push(...runLint({
2526
+ repository,
2527
+ pms,
2528
+ pmVersions,
2529
+ ruleSet: [rule],
2530
+ severityOverrides: overrides
2531
+ }).findings);
2532
+ else if (binding) findings.push(...evaluateBinding(repository, rule, binding, overrides));
2533
+ }
2534
+ }
2535
+ return findings;
2536
+ };
2537
+ //#endregion
2538
+ //#region src/core/rebase-finding.ts
2539
+ /** Context-local rule output becomes cwd-relative once, without mutating rule-owned data. */
2540
+ const rebaseFinding = (directory, finding) => {
2541
+ if (directory === ".") return {
2542
+ ...finding,
2543
+ directory
2544
+ };
2545
+ const remediation = finding.remediation;
2546
+ const rebaseOperation = (operation) => ({
2547
+ ...operation,
2548
+ file: {
2549
+ ...operation.file,
2550
+ path: `${directory}/${operation.file.path}`
2551
+ }
2552
+ });
2553
+ return {
2554
+ ...finding,
2555
+ directory,
2556
+ ...finding.file === void 0 ? {} : { file: `${directory}/${finding.file}` },
2557
+ remediation: remediation?.kind === "automatic" ? {
2558
+ ...remediation,
2559
+ operations: [rebaseOperation(remediation.operations[0]), ...remediation.operations.slice(1).map(rebaseOperation)]
2560
+ } : remediation?.kind === "manual" ? {
2561
+ ...remediation,
2562
+ steps: [`Work in ${directory} for this finding.`, ...remediation.steps]
2563
+ } : remediation
2564
+ };
2565
+ };
2566
+ //#endregion
2567
+ //#region src/core/lint.ts
2568
+ const validateLintOptions = (options, paths) => {
2699
2569
  if (!options || !paths.isAbsolute(options.cwd)) throw new UsageError("cwd must be an absolute filesystem path.");
2570
+ if ("workspaces" in options) throw new UsageError("workspaces was removed in 0.6.0. Package discovery is recursive by default; use exclude and installationRoots to specify scope.");
2700
2571
  if ("customRules" in options || "reporters" in options) throw new UsageError("Pass customRules and reporters inside the config option.");
2701
2572
  if (options.pm !== void 0 && !isPM(options.pm)) throw new UsageError(`Unknown package manager: ${String(options.pm)}`);
2702
2573
  if (options.projectType !== void 0 && !isProjectType(options.projectType)) throw new UsageError(`Unknown project type: ${String(options.projectType)}`);
2703
2574
  if (options.pmVersion !== void 0 && (!options.pm || !isStableVersion(options.pmVersion))) throw new UsageError("pmVersion / --pm-version requires pm / --pm and an exact stable version such as 10.16.0.");
2704
- if (options.workspaces !== void 0 && typeof options.workspaces !== "boolean") throw new UsageError("workspaces must be a boolean.");
2575
+ };
2576
+ /** Validate inputs and prepare shared contexts, without running rules or child executable configs. */
2577
+ const prepareLint = (options, dependencies) => {
2578
+ validateLintOptions(options, dependencies.paths);
2705
2579
  const config = options.config === void 0 ? void 0 : parseConfig(options.config);
2706
2580
  const fs = options.fs === void 0 ? dependencies.fileSystem : options.fs;
2707
- const ctx = createRepoContext(options.cwd, fs, options.projectType ?? config?.projectType);
2708
- const pms = resolvePMs(ctx, {
2709
- allowed: config?.pms,
2710
- pmOverride: options.pm
2711
- });
2712
- if (pms.includes("deno") && !ctx.exists(asRelPath("deno.json")) && ctx.exists(asRelPath("deno.jsonc"))) throw new ConfigError("deno.jsonc is not supported. siro currently reads strict JSON from deno.json only.");
2581
+ const excluded = dependencies.compileExclusions(parseExcludes(options.exclude !== void 0 ? options.exclude : config?.exclude ?? []));
2582
+ const installations = parseInstallationRoots(options.installationRoots !== void 0 ? options.installationRoots : config?.installationRoots ?? ["."]);
2713
2583
  const configured = applyConfig(dependencies.rules, config);
2584
+ const needsCustomTarget = configured.rules.some((rule) => scopeOf(rule.id) === "custom");
2585
+ const discovered = discover(options.cwd, fs, excluded, options.projectType ?? config?.projectType, dependencies);
2586
+ for (const entry of installations) if (!discovered.some((item) => item.directory === entry.path)) throw new UsageError(`${entry.path}: installation root must be an existing, selected ordinary directory using its exact enumerated spelling (no symlinks, excluded or hard-skipped paths).`);
2714
2587
  return {
2715
- ctx,
2716
- pms,
2717
- pmVersions: {
2718
- ...declaredPMVersion(ctx.packageJson?.packageManager),
2719
- ...config?.pmVersions,
2720
- ...options.pm && options.pmVersion ? { [options.pm]: options.pmVersion } : {}
2588
+ evaluation: {
2589
+ directories: discovered.map((item) => {
2590
+ const { ctx } = item.repository;
2591
+ const entry = installations.find((root) => root.path === item.directory);
2592
+ let pms;
2593
+ let versions = declaredPMVersion(ctx.packageJson?.packageManager);
2594
+ if (item.directory === ".") {
2595
+ pms = resolvePMs(ctx, {
2596
+ allowed: config?.pms,
2597
+ pmOverride: options.pm,
2598
+ optional: !entry && !needsCustomTarget
2599
+ });
2600
+ versions = {
2601
+ ...versions,
2602
+ ...config?.pmVersions,
2603
+ ...options.pm && options.pmVersion ? { [options.pm]: options.pmVersion } : {}
2604
+ };
2605
+ } else if (entry) {
2606
+ try {
2607
+ pms = resolvePMs(ctx, { pmOverride: entry.pm });
2608
+ } catch (error) {
2609
+ if (error instanceof UsageError) throw new UsageError(`${item.directory}: ${error.message}`);
2610
+ throw error;
2611
+ }
2612
+ if (entry.pm && entry.pmVersion) versions = {
2613
+ ...versions,
2614
+ [entry.pm]: entry.pmVersion
2615
+ };
2616
+ } else {
2617
+ const declared = ctx.packageJson?.packageManager === void 0 ? void 0 : parsePackageManagerField(ctx.packageJson.packageManager);
2618
+ pms = PMS.filter((pm) => pm === declared || pm === "deno" && item.manifests.includes("deno.json"));
2619
+ }
2620
+ return {
2621
+ ...item,
2622
+ installation: entry !== void 0,
2623
+ targets: pms.map((pm) => ({
2624
+ pm,
2625
+ ...versions[pm] === void 0 ? {} : { version: versions[pm] }
2626
+ }))
2627
+ };
2628
+ }),
2629
+ ruleSet: configured.rules,
2630
+ severityOverrides: configured.severityOverrides
2721
2631
  },
2722
- members: options.workspaces ? collectWorkspaceMembers(ctx, fs, pms, options.projectType ?? config?.projectType, dependencies) : [],
2723
- ruleSet: configured.rules,
2724
- severityOverrides: configured.severityOverrides,
2725
- codecFor,
2726
2632
  reporters: config?.reporters ?? []
2727
2633
  };
2728
2634
  };
2729
- /** Reuse the existing manifest checks; installation policy remains rooted at cwd. */
2730
- const runPreparedLint = (prepared) => {
2731
- const result = runLint(prepared);
2732
- const findings = [...result.findings];
2733
- const summary = { ...result.summary };
2734
- const memberRules = prepared.ruleSet.filter((rule) => [
2735
- "files-field",
2736
- "publish-access",
2737
- "unsupported-settings"
2738
- ].includes(rule.id));
2739
- for (const member of prepared.members) {
2740
- const child = runLint({
2741
- ...prepared,
2742
- ctx: member.ctx,
2743
- pms: [member.pm],
2744
- ruleSet: memberRules
2635
+ const runPreparedLint = (evaluation) => {
2636
+ const { directories, ruleSet, severityOverrides } = evaluation;
2637
+ const findings = [];
2638
+ const manifests = [];
2639
+ const installationRoots = [];
2640
+ const ruleOrder = new Map(ruleSet.map((rule, index) => [rule.id, index]));
2641
+ for (const item of directories) {
2642
+ const { directory, repository, targets, installation } = item;
2643
+ const local = [];
2644
+ for (const file of item.manifests) {
2645
+ const manifestTargets = targets.filter(({ pm }) => file === "deno.json" ? pm === "deno" : pm !== "deno");
2646
+ manifests.push({
2647
+ path: directory === "." ? file : `${directory}/${file}`,
2648
+ projectType: manifestProjectType(repository, file),
2649
+ targets: manifestTargets
2650
+ });
2651
+ local.push(...checkManifest(repository, file, manifestTargets, ruleSet, severityOverrides));
2652
+ }
2653
+ if (installation) installationRoots.push({
2654
+ directory,
2655
+ targets
2745
2656
  });
2746
- for (const finding of child.findings) findings.push({
2747
- ...finding,
2748
- file: `${member.directory}/${finding.file ?? "package.json"}`,
2749
- message: `${member.directory}: ${finding.message}`,
2750
- remediation: finding.remediation?.kind === "manual" ? {
2751
- kind: "manual",
2752
- steps: [`Work in ${member.directory} for this finding.`, ...finding.remediation.steps]
2753
- } : finding.remediation
2657
+ const scopedRules = ruleSet.flatMap((rule) => {
2658
+ const scope = scopeOf(rule.id);
2659
+ if (scope === "installation" && installation || scope === "custom" && directory === ".") return [rule];
2660
+ if (scope === "split" && installation) return [{
2661
+ ...rule,
2662
+ bindings: createUnsupportedSettings((file) => file !== "package.json" && file !== "deno.json").bindings
2663
+ }];
2664
+ return [];
2754
2665
  });
2755
- for (const level of [
2756
- "error",
2757
- "warn",
2758
- "info"
2759
- ]) summary[level] += child.summary[level];
2666
+ try {
2667
+ local.push(...runLint({
2668
+ repository,
2669
+ pms: targets.map(({ pm }) => pm),
2670
+ pmVersions: Object.fromEntries(targets.map(({ pm, version }) => [pm, version])),
2671
+ ruleSet: scopedRules,
2672
+ severityOverrides
2673
+ }).findings);
2674
+ } catch (error) {
2675
+ if (error instanceof ConfigError && directory !== ".") throw new ConfigError(`${directory}/${error.message}`);
2676
+ throw error;
2677
+ }
2678
+ local.sort((a, b) => (ruleOrder.get(a.ruleId) ?? 0) - (ruleOrder.get(b.ruleId) ?? 0));
2679
+ findings.push(...local.map((finding) => rebaseFinding(directory, finding)));
2760
2680
  }
2681
+ const summary = {
2682
+ error: 0,
2683
+ warn: 0,
2684
+ info: 0
2685
+ };
2686
+ for (const finding of findings) summary[finding.severity] += 1;
2761
2687
  return {
2762
2688
  findings,
2763
- summary
2689
+ summary,
2690
+ inspection: {
2691
+ manifests,
2692
+ installationRoots
2693
+ }
2764
2694
  };
2765
2695
  };
2766
- const lint$1 = (options, dependencies) => runPreparedLint(prepareLint(options, dependencies));
2696
+ const lint$1 = (options, dependencies) => runPreparedLint(prepareLint(options, dependencies).evaluation);
2767
2697
  //#endregion
2768
- //#region src/domain/services/filter.ts
2698
+ //#region src/core/filter.ts
2769
2699
  const EXIT_SUCCESS = 0;
2770
2700
  const EXIT_FAILURE = 1;
2771
2701
  const meetsThreshold = (severity, threshold) => SEVERITY_RANK[severity] >= SEVERITY_RANK[threshold];
@@ -2782,6 +2712,7 @@ const filterBySeverity = (result, threshold) => {
2782
2712
  summary[finding.severity] += 1;
2783
2713
  }
2784
2714
  return {
2715
+ ...result,
2785
2716
  findings,
2786
2717
  summary
2787
2718
  };
@@ -2795,7 +2726,7 @@ const exitCodeForLint = (result, threshold = "error") => {
2795
2726
  return EXIT_SUCCESS;
2796
2727
  };
2797
2728
  //#endregion
2798
- //#region src/application/commands/lint.ts
2729
+ //#region src/core/lint-command.ts
2799
2730
  /** Evaluate and report. Executable config loading belongs to the CLI adapter. */
2800
2731
  const lintCommand$1 = async (options, io, dependencies, reporters) => {
2801
2732
  if (!options) throw new UsageError("Lint options are required.");
@@ -2805,23 +2736,53 @@ const lintCommand$1 = async (options, io, dependencies, reporters) => {
2805
2736
  const selection = options.reporter ?? reporters.defaultName;
2806
2737
  const reporter = typeof selection === "string" ? registry.get(selection) : selection;
2807
2738
  if (!isReporterShape(reporter)) throw new UsageError(`${typeof selection === "string" ? "Unknown" : "Invalid"} reporter: ${String(selection)} (available: ${[...registry.keys()].join(", ")})`);
2808
- const result = runPreparedLint(prepared);
2739
+ const result = runPreparedLint(prepared.evaluation);
2809
2740
  const exitCode = exitCodeForLint(result, options.severity ?? "error");
2810
- await reporter.format(filterBySeverity(result, options.severity ?? "info"), io);
2741
+ const writes = [];
2742
+ let outputFailed = false;
2743
+ let outputFailure;
2744
+ const recordFailure = (error) => {
2745
+ if (!outputFailed) {
2746
+ outputFailed = true;
2747
+ outputFailure = error;
2748
+ }
2749
+ };
2750
+ const track = (write) => (line) => {
2751
+ try {
2752
+ const written = write(line);
2753
+ writes.push(Promise.resolve(written).then(() => {}, recordFailure));
2754
+ return written;
2755
+ } catch (error) {
2756
+ recordFailure(error);
2757
+ throw error;
2758
+ }
2759
+ };
2760
+ try {
2761
+ await reporter.format(filterBySeverity(result, options.severity ?? "info"), {
2762
+ stdout: track((line) => io.stdout(line)),
2763
+ stderr: track((line) => io.stderr(line))
2764
+ }, { cwd: options.cwd });
2765
+ } finally {
2766
+ await Promise.all(writes);
2767
+ }
2768
+ if (outputFailed) throw outputFailure;
2811
2769
  return exitCode;
2812
2770
  };
2813
2771
  //#endregion
2814
- //#region src/domain/schemas/package-json.ts
2772
+ //#region src/core/contracts/package-json.ts
2815
2773
  const PackageJsonSchema = vb.looseObject({
2816
2774
  files: vb.optional(vb.array(vb.string())),
2817
2775
  name: vb.optional(vb.string()),
2818
2776
  packageManager: vb.optional(vb.string()),
2819
2777
  private: vb.optional(vb.boolean()),
2820
- publishConfig: vb.optional(vb.looseObject({ access: vb.optional(vb.picklist([
2821
- "public",
2822
- "restricted",
2823
- "private"
2824
- ])) })),
2778
+ publishConfig: vb.optional(vb.looseObject({
2779
+ access: vb.optional(vb.picklist([
2780
+ "public",
2781
+ "restricted",
2782
+ "private"
2783
+ ])),
2784
+ provenance: vb.optional(vb.boolean())
2785
+ })),
2825
2786
  trustedDependencies: vb.optional(vb.array(vb.string()))
2826
2787
  });
2827
2788
  const parsePackageJson = (value) => {
@@ -2848,9 +2809,15 @@ const readPackageJson = (raw) => {
2848
2809
  };
2849
2810
  const createRepoContext = (root, fs = nodeFileSystem, projectType) => {
2850
2811
  if (fs === nodeFileSystem) assertDirectory(root);
2851
- const readText = (relPath) => fs.readText(resolveIn(root, relPath));
2812
+ const packageFile = resolveIn(root, asRelPath("package.json"));
2813
+ const raw = fs.readText(packageFile);
2814
+ const texts = new Map([[packageFile, raw]]);
2815
+ const readText = (relPath) => {
2816
+ const file = resolveIn(root, relPath);
2817
+ if (!texts.has(file)) texts.set(file, fs.readText(file));
2818
+ return texts.get(file);
2819
+ };
2852
2820
  const exists = (relPath) => fs.exists(resolveIn(root, relPath));
2853
- const raw = readText(asRelPath("package.json"));
2854
2821
  let packageJson = void 0;
2855
2822
  if (typeof raw !== "undefined") packageJson = readPackageJson(raw);
2856
2823
  return {
@@ -2917,33 +2884,40 @@ const escapeData = (raw) => raw.replaceAll("%", "%25").replaceAll("\r", "%0D").r
2917
2884
  const escapeProp = (raw) => escapeData(raw).replaceAll(":", "%3A").replaceAll(",", "%2C");
2918
2885
  /** Emit GitHub Actions workflow commands (annotations on PRs). */
2919
2886
  const githubReporter = {
2920
- format(result, io) {
2887
+ async format(result, io, context) {
2921
2888
  for (const finding of result.findings) {
2922
2889
  let file = "";
2923
- if (finding.file) file = `file=${escapeProp(finding.file)},`;
2924
- let body = `[${finding.pm}] ${finding.message}`;
2890
+ if (finding.file) file = `file=${escapeProp(path.resolve(context.cwd, finding.file))},`;
2891
+ let body = `[${finding.pm ?? "package"}] ${finding.directory}: ${finding.message}`;
2925
2892
  if (finding.docs) body += ` (${finding.docs})`;
2926
- io.stdout(`::${COMMAND[finding.severity]} ${file}title=${escapeProp(finding.ruleId)}::${escapeData(body)}`);
2893
+ await io.stdout(`::${COMMAND[finding.severity]} ${file}title=${escapeProp(finding.ruleId)}::${escapeData(body)}`);
2927
2894
  }
2928
2895
  },
2929
2896
  name: "github"
2930
2897
  };
2931
2898
  //#endregion
2899
+ //#region src/adapters/safe-text.ts
2900
+ /** Visible escaping for untrusted human-facing text; never rewrite API/native paths. */
2901
+ const safeText = (text) => text.replace(/[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069]/gu, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`).replaceAll("##[", "#\\u0023[").replace(/^(\s*)::/u, "$1\\u003a:");
2902
+ /** JSON syntax and decoded values stay identical; CI must not parse legacy commands in it. */
2903
+ const safeJsonText = (json) => json.replace(/[\u007f-\u009f\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069]/gu, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`).replaceAll("##[", "\\u0023\\u0023[");
2904
+ //#endregion
2932
2905
  //#region src/adapters/reporters/json.ts
2933
2906
  /**
2934
2907
  * Versioned machine-readable output — the public contract consumed by
2935
2908
  * external fixers (see docs/json-output.md). Bump `schemaVersion` on any
2936
2909
  * breaking shape change and update that doc in the same commit.
2937
2910
  */
2938
- const SCHEMA_VERSION = 2;
2911
+ const SCHEMA_VERSION = 3;
2939
2912
  const jsonReporter = {
2940
- format(result, io) {
2941
- io.stdout(JSON.stringify({
2913
+ async format(result, io) {
2914
+ await io.stdout(safeJsonText(JSON.stringify({
2942
2915
  schemaVersion: SCHEMA_VERSION,
2943
2916
  siroVersion: version,
2944
2917
  findings: result.findings,
2945
- summary: result.summary
2946
- }, void 0, 2));
2918
+ summary: result.summary,
2919
+ inspection: result.inspection
2920
+ }, void 0, 2)));
2947
2921
  },
2948
2922
  name: "json"
2949
2923
  };
@@ -2970,12 +2944,11 @@ const colorSupportedNow = () => {
2970
2944
  return pc.isColorSupported;
2971
2945
  };
2972
2946
  const renderFinding = (finding, ctx) => {
2973
- let where = "";
2974
- if (finding.file) where = ctx.colors.dim(` (${finding.file})`);
2975
- ctx.io.stdout(`${ctx.tag[finding.severity](GLYPH[finding.severity])} [${finding.pm}] ${ctx.colors.bold(finding.ruleId)}${where}`);
2976
- ctx.io.stdout(` ${finding.message}`);
2977
- for (const step of finding.remediation?.kind === "manual" ? finding.remediation.steps : []) ctx.io.stdout(` ↳ ${step}`);
2978
- if (finding.docs) ctx.io.stdout(ctx.colors.dim(` → ${finding.docs}`));
2947
+ const where = ctx.colors.dim(` (${safeText(finding.file ?? finding.directory)})`);
2948
+ ctx.io.stdout(`${ctx.tag[finding.severity](GLYPH[finding.severity])} [${finding.pm ?? "package"}] ${ctx.colors.bold(safeText(finding.ruleId))}${where}`);
2949
+ ctx.io.stdout(` ${safeText(finding.message)}`);
2950
+ for (const step of finding.remediation?.kind === "manual" ? finding.remediation.steps : []) ctx.io.stdout(` ↳ ${safeText(step)}`);
2951
+ if (finding.docs) ctx.io.stdout(ctx.colors.dim(` → ${safeText(finding.docs)}`));
2979
2952
  };
2980
2953
  const buildRenderCtx = (io) => {
2981
2954
  const colors = pc.createColors(colorSupportedNow());
@@ -2990,16 +2963,25 @@ const buildRenderCtx = (io) => {
2990
2963
  };
2991
2964
  };
2992
2965
  const prettyReporter = {
2993
- format(result, io) {
2994
- const ctx = buildRenderCtx(io);
2995
- if (result.findings.length === 0) {
2996
- io.stdout(ctx.colors.green("✔ No security best-practice issues found."));
2997
- return;
2966
+ async format(result, io) {
2967
+ const lines = [];
2968
+ const collect = (line) => {
2969
+ lines.push(line);
2970
+ };
2971
+ const ctx = buildRenderCtx({
2972
+ stdout: collect,
2973
+ stderr: collect
2974
+ });
2975
+ collect(`Inspection: ${result.inspection.manifests.length} manifests; installation roots: ${result.inspection.installationRoots.map((root) => safeText(root.directory)).join(", ") || "none"}.`);
2976
+ collect("Unknown PM/version targets have no availability assessment; installation scope is explicit.");
2977
+ if (result.findings.length === 0) collect(ctx.colors.green("✔ No security best-practice issues found."));
2978
+ else {
2979
+ for (const finding of result.findings) renderFinding(finding, ctx);
2980
+ const { error, warn, info } = result.summary;
2981
+ collect("");
2982
+ collect(`Summary: ${error} error, ${warn} warn, ${info} info`);
2998
2983
  }
2999
- for (const finding of result.findings) renderFinding(finding, ctx);
3000
- const { error, warn, info } = result.summary;
3001
- io.stdout("");
3002
- io.stdout(`Summary: ${error} error, ${warn} warn, ${info} info`);
2984
+ await io.stdout(lines.join("\n"));
3003
2985
  },
3004
2986
  name: "pretty"
3005
2987
  };
@@ -3016,16 +2998,16 @@ const JSON_REPORTER_NAME = jsonReporter.name;
3016
2998
  const BUILTIN_REPORTER_NAMES = BUILTINS.map((rep) => rep.name);
3017
2999
  /** Later registrations replace earlier reporters with the same name. */
3018
3000
  const createRegistry = (extras = []) => new Map([...BUILTINS, ...extras].map((reporter) => [reporter.name, reporter]));
3019
- //#endregion
3020
- //#region src/composition/lint.ts
3021
3001
  const dependencies = {
3022
- rules,
3002
+ rules: createBuiltinRules({
3003
+ now: () => Date.now(),
3004
+ parse: (value) => Date.parse(value)
3005
+ }),
3023
3006
  fileSystem: nodeFileSystem,
3024
3007
  paths: nodePaths,
3025
3008
  createRepoContext,
3026
3009
  codecFor,
3027
- globs: minimatchGlobs,
3028
- caseInsensitiveGlobs: process.platform === "darwin" || process.platform === "win32"
3010
+ compileExclusions
3029
3011
  };
3030
3012
  /** Public Node API: callers can replace the filesystem without assembling the application. */
3031
3013
  const lint = (options) => lint$1(options, dependencies);
@@ -3035,11 +3017,47 @@ const lintCommand = (options, io) => lintCommand$1(options, io, dependencies, {
3035
3017
  });
3036
3018
  //#endregion
3037
3019
  //#region src/adapters/node-io.ts
3038
- const nodeIO = {
3039
- stderr: (line) => process.stderr.write(`${line}\n`),
3040
- stdout: (line) => process.stdout.write(`${line}\n`)
3020
+ const writer = (stream) => {
3021
+ let failure;
3022
+ const pending = /* @__PURE__ */ new Set();
3023
+ const fail = (cause) => {
3024
+ failure ??= new Error(`Output failed: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
3025
+ for (const reject of pending) reject(failure);
3026
+ pending.clear();
3027
+ return failure;
3028
+ };
3029
+ let observing = false;
3030
+ return (line) => {
3031
+ if (!observing) {
3032
+ observing = true;
3033
+ stream.on("error", fail);
3034
+ stream.on("close", () => fail(/* @__PURE__ */ new Error("Output stream closed.")));
3035
+ }
3036
+ return new Promise((resolve, reject) => {
3037
+ if (failure || stream.destroyed) {
3038
+ reject(failure ?? fail(/* @__PURE__ */ new Error("Output stream is destroyed.")));
3039
+ return;
3040
+ }
3041
+ pending.add(reject);
3042
+ try {
3043
+ stream.write(`${line}\n`, (error) => {
3044
+ pending.delete(reject);
3045
+ if (error) reject(fail(error));
3046
+ else resolve();
3047
+ });
3048
+ } catch (error) {
3049
+ fail(error);
3050
+ }
3051
+ });
3052
+ };
3041
3053
  };
3054
+ /** Node-only construction; keep streams and their errors outside the core. */
3055
+ const createNodeIO = (stdout, stderr) => ({
3056
+ stdout: writer(stdout),
3057
+ stderr: writer(stderr)
3058
+ });
3059
+ const nodeIO = createNodeIO(process.stdout, process.stderr);
3042
3060
  //#endregion
3043
- export { ConfigError as A, SUPPORTED_NODE_RANGE as C, isNodeError as D, nodeFileSystem as E, UsageError as M, asAbsPath as O, isSupportedNodeVersion as S, assertDirectory as T, isProjectType as _, DEFAULT_REPORTER_NAME as a, isPM as b, jsonReporter as c, requireConfigKey as d, CONFIG_FILES as f, PROJECT_TYPES as g, getByPath as h, BUILTIN_REPORTER_NAMES as i, SiroError as j, asRelPath as k, githubReporter as l, defineRule as m, lint as n, JSON_REPORTER_NAME as o, loadConfig as p, lintCommand as r, prettyReporter as s, nodeIO as t, overrideBindings as u, PMS as v, version as w, isSeverity as x, SEVERITIES as y };
3061
+ export { asRelPath as A, isSupportedNodeVersion as C, nodeFileSystem as D, assertDirectory as E, SiroError as M, UsageError as N, isNodeError as O, isSeverity as S, version as T, PROJECT_TYPES as _, DEFAULT_REPORTER_NAME as a, SEVERITIES as b, jsonReporter as c, overrideBindings as d, requireConfigKey as f, getByPath as g, defineRule as h, BUILTIN_REPORTER_NAMES as i, ConfigError as j, asAbsPath as k, safeText as l, loadConfig as m, lint as n, JSON_REPORTER_NAME as o, CONFIG_FILES as p, lintCommand as r, prettyReporter as s, nodeIO as t, githubReporter as u, isProjectType as v, SUPPORTED_NODE_RANGE as w, isPM as x, PMS as y };
3044
3062
 
3045
- //# sourceMappingURL=node-io-BDHg4J2w.mjs.map
3063
+ //# sourceMappingURL=node-io-CAszdmko.mjs.map