@xenos1996/usa 2.0.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.
Files changed (163) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +335 -0
  3. package/USA.md +597 -0
  4. package/dist/bootstrap/catalog.d.ts +46 -0
  5. package/dist/bootstrap/catalog.d.ts.map +1 -0
  6. package/dist/bootstrap/catalog.js +312 -0
  7. package/dist/bootstrap/catalog.js.map +1 -0
  8. package/dist/bootstrap/index.d.ts +36 -0
  9. package/dist/bootstrap/index.d.ts.map +1 -0
  10. package/dist/bootstrap/index.js +177 -0
  11. package/dist/bootstrap/index.js.map +1 -0
  12. package/dist/cli.d.ts +5 -0
  13. package/dist/cli.d.ts.map +1 -0
  14. package/dist/cli.js +706 -0
  15. package/dist/cli.js.map +1 -0
  16. package/dist/config.d.ts +7 -0
  17. package/dist/config.d.ts.map +1 -0
  18. package/dist/config.js +88 -0
  19. package/dist/config.js.map +1 -0
  20. package/dist/detect/index.d.ts +58 -0
  21. package/dist/detect/index.d.ts.map +1 -0
  22. package/dist/detect/index.js +329 -0
  23. package/dist/detect/index.js.map +1 -0
  24. package/dist/engine/audit.d.ts +32 -0
  25. package/dist/engine/audit.d.ts.map +1 -0
  26. package/dist/engine/audit.js +366 -0
  27. package/dist/engine/audit.js.map +1 -0
  28. package/dist/engine/diff.d.ts +24 -0
  29. package/dist/engine/diff.d.ts.map +1 -0
  30. package/dist/engine/diff.js +136 -0
  31. package/dist/engine/diff.js.map +1 -0
  32. package/dist/engine/evaluate.d.ts +19 -0
  33. package/dist/engine/evaluate.d.ts.map +1 -0
  34. package/dist/engine/evaluate.js +377 -0
  35. package/dist/engine/evaluate.js.map +1 -0
  36. package/dist/engine/gate.d.ts +19 -0
  37. package/dist/engine/gate.d.ts.map +1 -0
  38. package/dist/engine/gate.js +47 -0
  39. package/dist/engine/gate.js.map +1 -0
  40. package/dist/engine/loader.d.ts +37 -0
  41. package/dist/engine/loader.d.ts.map +1 -0
  42. package/dist/engine/loader.js +388 -0
  43. package/dist/engine/loader.js.map +1 -0
  44. package/dist/engine/maturity.d.ts +20 -0
  45. package/dist/engine/maturity.d.ts.map +1 -0
  46. package/dist/engine/maturity.js +157 -0
  47. package/dist/engine/maturity.js.map +1 -0
  48. package/dist/engine/score.d.ts +19 -0
  49. package/dist/engine/score.d.ts.map +1 -0
  50. package/dist/engine/score.js +141 -0
  51. package/dist/engine/score.js.map +1 -0
  52. package/dist/engine/sections.d.ts +14 -0
  53. package/dist/engine/sections.d.ts.map +1 -0
  54. package/dist/engine/sections.js +50 -0
  55. package/dist/engine/sections.js.map +1 -0
  56. package/dist/evolution/benchmark.d.ts +12 -0
  57. package/dist/evolution/benchmark.d.ts.map +1 -0
  58. package/dist/evolution/benchmark.js +107 -0
  59. package/dist/evolution/benchmark.js.map +1 -0
  60. package/dist/evolution/capability.d.ts +22 -0
  61. package/dist/evolution/capability.d.ts.map +1 -0
  62. package/dist/evolution/capability.js +69 -0
  63. package/dist/evolution/capability.js.map +1 -0
  64. package/dist/evolution/coverage.d.ts +15 -0
  65. package/dist/evolution/coverage.d.ts.map +1 -0
  66. package/dist/evolution/coverage.js +65 -0
  67. package/dist/evolution/coverage.js.map +1 -0
  68. package/dist/evolution/gap.d.ts +10 -0
  69. package/dist/evolution/gap.d.ts.map +1 -0
  70. package/dist/evolution/gap.js +52 -0
  71. package/dist/evolution/gap.js.map +1 -0
  72. package/dist/evolution/propose.d.ts +50 -0
  73. package/dist/evolution/propose.d.ts.map +1 -0
  74. package/dist/evolution/propose.js +188 -0
  75. package/dist/evolution/propose.js.map +1 -0
  76. package/dist/evolution/queue.d.ts +64 -0
  77. package/dist/evolution/queue.d.ts.map +1 -0
  78. package/dist/evolution/queue.js +192 -0
  79. package/dist/evolution/queue.js.map +1 -0
  80. package/dist/evolution/release.d.ts +11 -0
  81. package/dist/evolution/release.d.ts.map +1 -0
  82. package/dist/evolution/release.js +55 -0
  83. package/dist/evolution/release.js.map +1 -0
  84. package/dist/evolution/run.d.ts +88 -0
  85. package/dist/evolution/run.d.ts.map +1 -0
  86. package/dist/evolution/run.js +258 -0
  87. package/dist/evolution/run.js.map +1 -0
  88. package/dist/evolution/schedule.d.ts +59 -0
  89. package/dist/evolution/schedule.d.ts.map +1 -0
  90. package/dist/evolution/schedule.js +77 -0
  91. package/dist/evolution/schedule.js.map +1 -0
  92. package/dist/evolution/types.d.ts +174 -0
  93. package/dist/evolution/types.d.ts.map +1 -0
  94. package/dist/evolution/types.js +8 -0
  95. package/dist/evolution/types.js.map +1 -0
  96. package/dist/index.d.ts +36 -0
  97. package/dist/index.d.ts.map +1 -0
  98. package/dist/index.js +30 -0
  99. package/dist/index.js.map +1 -0
  100. package/dist/learn/index.d.ts +27 -0
  101. package/dist/learn/index.d.ts.map +1 -0
  102. package/dist/learn/index.js +196 -0
  103. package/dist/learn/index.js.map +1 -0
  104. package/dist/report/markdown.d.ts +16 -0
  105. package/dist/report/markdown.d.ts.map +1 -0
  106. package/dist/report/markdown.js +488 -0
  107. package/dist/report/markdown.js.map +1 -0
  108. package/dist/snapshot/index.d.ts +55 -0
  109. package/dist/snapshot/index.d.ts.map +1 -0
  110. package/dist/snapshot/index.js +76 -0
  111. package/dist/snapshot/index.js.map +1 -0
  112. package/dist/store/index.d.ts +31 -0
  113. package/dist/store/index.d.ts.map +1 -0
  114. package/dist/store/index.js +69 -0
  115. package/dist/store/index.js.map +1 -0
  116. package/dist/types.d.ts +287 -0
  117. package/dist/types.d.ts.map +1 -0
  118. package/dist/types.js +16 -0
  119. package/dist/types.js.map +1 -0
  120. package/dist/util/glob.d.ts +11 -0
  121. package/dist/util/glob.d.ts.map +1 -0
  122. package/dist/util/glob.js +86 -0
  123. package/dist/util/glob.js.map +1 -0
  124. package/dist/util/project.d.ts +90 -0
  125. package/dist/util/project.d.ts.map +1 -0
  126. package/dist/util/project.js +636 -0
  127. package/dist/util/project.js.map +1 -0
  128. package/dist/util/yaml.d.ts +28 -0
  129. package/dist/util/yaml.d.ts.map +1 -0
  130. package/dist/util/yaml.js +48 -0
  131. package/dist/util/yaml.js.map +1 -0
  132. package/package.json +94 -0
  133. package/rules/core/architecture.yaml +190 -0
  134. package/rules/core/cicd.yaml +212 -0
  135. package/rules/core/code-quality.yaml +261 -0
  136. package/rules/core/dependencies.yaml +132 -0
  137. package/rules/core/documentation.yaml +119 -0
  138. package/rules/core/future-readiness.yaml +91 -0
  139. package/rules/core/release.yaml +131 -0
  140. package/rules/core/repo.yaml +313 -0
  141. package/rules/core/security.yaml +496 -0
  142. package/rules/core/supply-chain.yaml +357 -0
  143. package/rules/core/testing.yaml +266 -0
  144. package/rules/detectors.yaml +2028 -0
  145. package/rules/index.yaml +42 -0
  146. package/rules/profiles/maturity.yaml +135 -0
  147. package/rules/stacks/ai-era.yaml +193 -0
  148. package/rules/stacks/api-backend.yaml +121 -0
  149. package/rules/stacks/cli.yaml +105 -0
  150. package/rules/stacks/compliance.yaml +157 -0
  151. package/rules/stacks/containers.yaml +124 -0
  152. package/rules/stacks/data.yaml +165 -0
  153. package/rules/stacks/go.yaml +134 -0
  154. package/rules/stacks/iac.yaml +128 -0
  155. package/rules/stacks/jvm.yaml +150 -0
  156. package/rules/stacks/ml-ai.yaml +140 -0
  157. package/rules/stacks/mobile.yaml +121 -0
  158. package/rules/stacks/node-typescript.yaml +166 -0
  159. package/rules/stacks/python.yaml +175 -0
  160. package/rules/stacks/rust.yaml +129 -0
  161. package/rules/stacks/solidity.yaml +139 -0
  162. package/rules/stacks/swift.yaml +100 -0
  163. package/rules/stacks/web-frontend.yaml +159 -0
package/dist/cli.js ADDED
@@ -0,0 +1,706 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
+ import { runAudit } from './engine/audit.js';
6
+ import { loadRulePacks, loadPackFile } from './engine/loader.js';
7
+ import { loadDetectorFile } from './detect/index.js';
8
+ import { Project } from './util/project.js';
9
+ import { detect } from './detect/index.js';
10
+ import { renderMarkdown, parseTrailer } from './report/markdown.js';
11
+ import { diffReports } from './engine/diff.js';
12
+ import { bootstrapPacks, writeBootstrapPacks } from './bootstrap/index.js';
13
+ import { EXAMPLE_CONFIG, loadConfig } from './config.js';
14
+ import { evaluateGate } from './engine/gate.js';
15
+ import { learnFromReport, renderSuggestions } from './learn/index.js';
16
+ import { Store } from './store/index.js';
17
+ import { runEvolutionCycle } from './evolution/run.js';
18
+ import { capabilityFromPack } from './evolution/capability.js';
19
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
20
+ const DEFAULT_RULES_DIR = path.resolve(HERE, '..', 'rules');
21
+ const VERSION = readVersion();
22
+ const SEVERITY_ORDER = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'FUTURE'];
23
+ const DEPTHS = ['quick', 'standard', 'deep'];
24
+ const MATURITIES = ['prototype', 'mvp', 'beta', 'production', 'legacy'];
25
+ function parseArgs(argv) {
26
+ const args = { _: [] };
27
+ for (let i = 0; i < argv.length; i++) {
28
+ const tok = argv[i];
29
+ if (!tok.startsWith('--')) {
30
+ args._.push(tok);
31
+ continue;
32
+ }
33
+ const body = tok.slice(2);
34
+ if (body.includes('=')) {
35
+ const [k, ...rest] = body.split('=');
36
+ args[k] = rest.join('=');
37
+ continue;
38
+ }
39
+ const next = argv[i + 1];
40
+ if (next === undefined || next.startsWith('--')) {
41
+ args[body] = true;
42
+ }
43
+ else {
44
+ args[body] = next;
45
+ i++;
46
+ }
47
+ }
48
+ return args;
49
+ }
50
+ function str(args, key, fallback) {
51
+ const v = args[key];
52
+ return typeof v === 'string' ? v : fallback;
53
+ }
54
+ function bool(args, key) {
55
+ return args[key] === true || args[key] === 'true';
56
+ }
57
+ function list(args, key) {
58
+ const v = args[key];
59
+ if (Array.isArray(v))
60
+ return v;
61
+ if (typeof v === 'string')
62
+ return v
63
+ .split(',')
64
+ .map((s) => s.trim())
65
+ .filter(Boolean);
66
+ return [];
67
+ }
68
+ /* -------------------------------------------------------------------- main -- */
69
+ const COMMANDS = {
70
+ audit: cmdAudit,
71
+ detect: cmdDetect,
72
+ rules: cmdRules,
73
+ explain: cmdExplain,
74
+ diff: cmdDiff,
75
+ init: cmdInit,
76
+ bootstrap: cmdBootstrap,
77
+ learn: cmdLearn,
78
+ evolve: cmdEvolve,
79
+ };
80
+ export function main(argv) {
81
+ const args = parseArgs(argv);
82
+ if (bool(args, 'help')) {
83
+ console.log(HELP);
84
+ return 0;
85
+ }
86
+ if (bool(args, 'version')) {
87
+ console.log(`usa ${VERSION}`);
88
+ return 0;
89
+ }
90
+ const cmd = (args._[0] ?? 'audit');
91
+ const run = COMMANDS[cmd];
92
+ if (run)
93
+ return run(args);
94
+ switch (cmd) {
95
+ case 'version':
96
+ case '--version':
97
+ console.log(`usa ${VERSION}`);
98
+ return 0;
99
+ case 'help':
100
+ case '--help':
101
+ case '-h':
102
+ console.log(HELP);
103
+ return 0;
104
+ default:
105
+ console.error(`Unknown command: ${cmd}\n`);
106
+ console.error(HELP);
107
+ return 2;
108
+ }
109
+ }
110
+ function parseCountFlag(v) {
111
+ if (v === undefined)
112
+ return undefined;
113
+ const n = Number(v);
114
+ return Number.isFinite(n) ? n : NaN;
115
+ }
116
+ function readAuditOptions(args) {
117
+ return {
118
+ target: args._[1] ?? '.',
119
+ rulesDir: path.resolve(str(args, 'rules-dir', DEFAULT_RULES_DIR) ?? DEFAULT_RULES_DIR),
120
+ depth: str(args, 'depth', 'standard') ?? 'standard',
121
+ profileArg: str(args, 'profile', 'auto') ?? 'auto',
122
+ out: str(args, 'out', 'AUDIT.md') ?? 'AUDIT.md',
123
+ failOn: (str(args, 'fail-on', 'none') ?? 'none').toLowerCase(),
124
+ quiet: bool(args, 'quiet'),
125
+ maxFiles: parseCountFlag(str(args, 'max-files')),
126
+ maxBytes: parseCountFlag(str(args, 'max-bytes')),
127
+ };
128
+ }
129
+ function validateAuditOptions(o) {
130
+ if (!DEPTHS.includes(o.depth))
131
+ return `--depth must be one of ${DEPTHS.join('|')}`;
132
+ if (o.profileArg !== 'auto' && !MATURITIES.includes(o.profileArg)) {
133
+ return `--profile must be auto or one of ${MATURITIES.join('|')}`;
134
+ }
135
+ if (o.maxFiles !== undefined && !(o.maxFiles > 0))
136
+ return '--max-files must be a positive number';
137
+ if (o.maxBytes !== undefined && !(o.maxBytes > 0))
138
+ return '--max-bytes must be a positive number';
139
+ return null;
140
+ }
141
+ function cmdAudit(args) {
142
+ const o = readAuditOptions(args);
143
+ const invalid = validateAuditOptions(o);
144
+ if (invalid) {
145
+ console.error(invalid);
146
+ return 2;
147
+ }
148
+ if (!fs.existsSync(o.target)) {
149
+ console.error(`Target path does not exist: ${o.target}`);
150
+ return 2;
151
+ }
152
+ let config;
153
+ try {
154
+ config = loadConfig(o.target, str(args, 'config'));
155
+ }
156
+ catch (err) {
157
+ console.error(err.message);
158
+ return 2;
159
+ }
160
+ if (config.facts)
161
+ config.facts = [...config.facts, ...list(args, 'fact')];
162
+ const { report, warnings, profile } = runAudit({
163
+ target: o.target,
164
+ rulesDir: o.rulesDir,
165
+ depth: o.depth,
166
+ profile: o.profileArg,
167
+ config,
168
+ allowCommands: bool(args, 'allow-commands'),
169
+ usaVersion: VERSION,
170
+ includePacks: list(args, 'include'),
171
+ excludePacks: list(args, 'exclude'),
172
+ maxFiles: o.maxFiles,
173
+ maxBytes: o.maxBytes,
174
+ });
175
+ for (const w of warnings)
176
+ console.error(`warning: ${w}`);
177
+ const markdown = renderMarkdown(report, profile);
178
+ fs.writeFileSync(o.out, markdown, 'utf8');
179
+ if (!o.quiet) {
180
+ console.log(summaryLine(report));
181
+ console.log(` report → ${o.out}`);
182
+ }
183
+ return evaluateGate(report, o.failOn, o.quiet);
184
+ }
185
+ function summaryLine(report) {
186
+ const s = report.score.severityCounts;
187
+ const parts = SEVERITY_ORDER.filter((k) => s[k] > 0)
188
+ .map((k) => `${k.toLowerCase()} ${s[k]}`)
189
+ .join(' · ');
190
+ return (`usa ${report.score.overall}/100 (${report.detection.maturity}) — ` +
191
+ `${report.score.counts.PASS} passed, ` +
192
+ (parts ? `open: ${parts}` : 'no open findings') +
193
+ `, ${report.score.counts.UNKNOWN} to review` +
194
+ ` (${report.score.automationCoverage}% verified automatically)`);
195
+ }
196
+ /* ----------------------------------------------------------------- detect -- */
197
+ function cmdDetect(args) {
198
+ const target = args._[1] ?? '.';
199
+ const rulesDir = path.resolve(str(args, 'rules-dir', DEFAULT_RULES_DIR) ?? DEFAULT_RULES_DIR);
200
+ const config = loadConfig(target, str(args, 'config'));
201
+ const project = new Project(target, config.ignore ?? []);
202
+ const git = project.gitInfo();
203
+ const detection = detect(project, loadDetectorFile(rulesDir), git, [
204
+ ...list(args, 'fact'),
205
+ ...(config.facts ?? []),
206
+ ]);
207
+ const grouped = groupFlagsByNamespace(detection.facts.flags);
208
+ console.log(`# Detection: ${path.resolve(target)}`);
209
+ console.log();
210
+ console.log(`maturity: ${detection.maturity}`);
211
+ for (const [ns, values] of [...grouped.entries()].sort()) {
212
+ console.log(`${ns}: ${[...new Set(values)].sort().join(', ')}`);
213
+ }
214
+ console.log();
215
+ console.log('metrics:');
216
+ for (const [k, v] of Object.entries(detection.facts.metrics))
217
+ console.log(` ${k}: ${v}`);
218
+ console.log();
219
+ console.log('maturity signals:');
220
+ for (const s of detection.maturitySignals)
221
+ console.log(` ${s}`);
222
+ return 0;
223
+ }
224
+ function groupFlagsByNamespace(flags) {
225
+ const grouped = new Map();
226
+ for (const f of flags) {
227
+ const [ns, value] = f.split(':');
228
+ const key = value === undefined ? 'other' : ns;
229
+ const list2 = grouped.get(key) ?? [];
230
+ list2.push(value === undefined ? f : value);
231
+ grouped.set(key, list2);
232
+ }
233
+ return grouped;
234
+ }
235
+ /* ------------------------------------------------------------------ rules -- */
236
+ function cmdRules(args) {
237
+ const rulesDir = path.resolve(str(args, 'rules-dir', DEFAULT_RULES_DIR) ?? DEFAULT_RULES_DIR);
238
+ const { packs } = loadRulePacks(rulesDir);
239
+ const filter = str(args, 'section');
240
+ let total = 0;
241
+ for (const pack of packs) {
242
+ const rules = filter ? pack.rules.filter((r) => r.section === filter) : pack.rules;
243
+ if (filter && rules.length === 0)
244
+ continue;
245
+ console.log(`\n## ${pack.id} — ${pack.title} (${rules.length} rules)`);
246
+ if (pack.description)
247
+ console.log(` ${pack.description}`);
248
+ for (const r of rules) {
249
+ total++;
250
+ console.log(` ${r.id.padEnd(10)} ${r.severity.padEnd(8)} ${r.ruleClass.padEnd(16)} ${r.title}`);
251
+ }
252
+ }
253
+ console.log(`\n${total} rule(s) across ${packs.length} pack(s).`);
254
+ return 0;
255
+ }
256
+ /* ---------------------------------------------------------------- explain -- */
257
+ function cmdExplain(args) {
258
+ const id = args._[1];
259
+ if (!id) {
260
+ console.error('Usage: usa explain <RULE-ID>');
261
+ return 2;
262
+ }
263
+ const rulesDir = path.resolve(str(args, 'rules-dir', DEFAULT_RULES_DIR) ?? DEFAULT_RULES_DIR);
264
+ const { packs } = loadRulePacks(rulesDir);
265
+ for (const pack of packs) {
266
+ const rule = pack.rules.find((r) => r.id.toLowerCase() === id.toLowerCase());
267
+ if (!rule)
268
+ continue;
269
+ printRuleDetail(pack.id, rule);
270
+ return 0;
271
+ }
272
+ console.error(`Rule not found: ${id}`);
273
+ return 1;
274
+ }
275
+ function printRuleDetail(packId, rule) {
276
+ console.log(`# ${rule.id} — ${rule.title}`);
277
+ console.log();
278
+ console.log(`pack : ${packId}`);
279
+ console.log(`section : ${rule.section} ${rule.sectionTitle ?? ''}`);
280
+ console.log(`severity : ${rule.severity} (when violated)`);
281
+ console.log(`class : ${rule.ruleClass}`);
282
+ console.log(`weight : ${rule.weight ?? 'default'}`);
283
+ console.log(`depths : ${rule.depths?.join(', ') ?? 'all'}`);
284
+ console.log(`check : ${JSON.stringify(rule.check)}`);
285
+ printRuleOptional(rule);
286
+ }
287
+ function printRuleOptional(rule) {
288
+ if (rule.appliesWhen)
289
+ console.log(`applies : ${JSON.stringify(rule.appliesWhen)}`);
290
+ if (rule.why)
291
+ console.log(`\nwhy:\n ${rule.why}`);
292
+ if (rule.evidence)
293
+ console.log(`\nevidence:\n ${rule.evidence}`);
294
+ if (rule.remediation)
295
+ console.log(`\nfix:\n ${rule.remediation}`);
296
+ if (rule.references?.length)
297
+ console.log(`\nreferences:\n ${rule.references.join('\n ')}`);
298
+ }
299
+ /* ------------------------------------------------------------------- diff -- */
300
+ function cmdDiff(args) {
301
+ const beforePath = args._[1];
302
+ const afterPath = args._[2];
303
+ if (!beforePath || !afterPath) {
304
+ console.error('Usage: usa diff <before.md> <after.md> [--out DIFF.md]');
305
+ return 2;
306
+ }
307
+ const readTrailer = (file) => {
308
+ const text = fs.readFileSync(file, 'utf8');
309
+ return parseTrailer(text) ?? text; // tolerate a raw YAML trailer file
310
+ };
311
+ try {
312
+ const md = diffReports(readTrailer(beforePath), readTrailer(afterPath));
313
+ const out = str(args, 'out');
314
+ if (out)
315
+ fs.writeFileSync(out, md, 'utf8');
316
+ console.log(md);
317
+ return 0;
318
+ }
319
+ catch (err) {
320
+ console.error(err.message);
321
+ return 2;
322
+ }
323
+ }
324
+ /* -------------------------------------------------------------- bootstrap -- */
325
+ function cmdBootstrap(args) {
326
+ const target = args._[1] ?? '.';
327
+ const rulesDir = path.resolve(str(args, 'rules-dir', DEFAULT_RULES_DIR) ?? DEFAULT_RULES_DIR);
328
+ const config = loadConfig(target, str(args, 'config'));
329
+ const project = new Project(target, config.ignore ?? []);
330
+ const git = project.gitInfo();
331
+ const detection = detect(project, loadDetectorFile(rulesDir), git, [
332
+ ...list(args, 'fact'),
333
+ ...(config.facts ?? []),
334
+ ]);
335
+ const outcome = bootstrapPacks(detection.facts);
336
+ if (outcome.packs.length === 0) {
337
+ console.log('No uncovered stacks: every detected language already has a pack.');
338
+ return 0;
339
+ }
340
+ const out = str(args, 'out');
341
+ if (!out) {
342
+ printBootstrap(outcome);
343
+ return 0;
344
+ }
345
+ try {
346
+ writeBootstrapPacks(outcome, out);
347
+ }
348
+ catch (err) {
349
+ console.error(err.message);
350
+ return 2;
351
+ }
352
+ console.log(`wrote ${outcome.packs.length} pack(s) to ${out}`);
353
+ for (const note of outcome.notes)
354
+ console.log(`- ${note}`);
355
+ return 0;
356
+ }
357
+ function printBootstrap(outcome) {
358
+ for (const pack of outcome.packs) {
359
+ console.log(`--- ${pack.filename} ---`);
360
+ console.log(pack.yaml);
361
+ }
362
+ for (const note of outcome.notes)
363
+ console.log(`- ${note}`);
364
+ }
365
+ /* ------------------------------------------------------------------- init -- */
366
+ function cmdInit(args) {
367
+ const target = args._[1] ?? '.';
368
+ fs.mkdirSync(target, { recursive: true });
369
+ const configPath = path.join(target, '.usa.yaml');
370
+ if (fs.existsSync(configPath)) {
371
+ console.error(`${configPath} already exists — leaving it alone.`);
372
+ }
373
+ else {
374
+ fs.writeFileSync(configPath, EXAMPLE_CONFIG, 'utf8');
375
+ console.log(`created ${configPath}`);
376
+ }
377
+ const workflowDir = path.join(target, '.github', 'workflows');
378
+ const workflowPath = path.join(workflowDir, 'usa.yml');
379
+ if (fs.existsSync(workflowPath)) {
380
+ console.error(`${workflowPath} already exists — leaving it alone.`);
381
+ }
382
+ else {
383
+ fs.mkdirSync(workflowDir, { recursive: true });
384
+ fs.writeFileSync(workflowPath, WORKFLOW_TEMPLATE, 'utf8');
385
+ console.log(`created ${workflowPath}`);
386
+ }
387
+ console.log('\nNext: run `npx usa audit .` or `usa audit . --depth standard`.');
388
+ return 0;
389
+ }
390
+ /* ------------------------------------------------------------------ learn -- */
391
+ function cmdLearn(args) {
392
+ const reportPath = args._[1];
393
+ if (!reportPath) {
394
+ console.error('Usage: usa learn <report.md> [--out <file>] [--min-severity MEDIUM]');
395
+ return 2;
396
+ }
397
+ if (!fs.existsSync(reportPath)) {
398
+ console.error(`Report file not found: ${reportPath}`);
399
+ return 2;
400
+ }
401
+ const outFile = str(args, 'out', 'learn-suggestions.yaml') ?? 'learn-suggestions.yaml';
402
+ const minSeverity = (str(args, 'min-severity', 'MEDIUM') ?? 'MEDIUM').toUpperCase();
403
+ const allowed = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'FUTURE'];
404
+ if (!allowed.includes(minSeverity)) {
405
+ console.error(`--min-severity must be one of: ${allowed.join(', ')}`);
406
+ return 2;
407
+ }
408
+ const options = { reportPath, out: outFile, minSeverity };
409
+ try {
410
+ const suggestions = learnFromReport(options);
411
+ if (suggestions.length === 0) {
412
+ console.log(`No actionable suggestions found in ${reportPath}.`);
413
+ return 0;
414
+ }
415
+ const yaml = renderSuggestions(suggestions);
416
+ fs.writeFileSync(outFile, yaml, 'utf8');
417
+ console.log(`Wrote ${suggestions.length} suggestion(s) to ${outFile}`);
418
+ console.log(' Review the rules, test them, then register in rules/index.yaml.');
419
+ return 0;
420
+ }
421
+ catch (err) {
422
+ console.error(err.message);
423
+ return 2;
424
+ }
425
+ }
426
+ /* ----------------------------------------------------------------- evolve -- */
427
+ function cmdEvolve(args) {
428
+ const target = args._[1] ?? '.';
429
+ const rulesDir = path.resolve(str(args, 'rules-dir', DEFAULT_RULES_DIR) ?? DEFAULT_RULES_DIR);
430
+ if (!fs.existsSync(target)) {
431
+ console.error(`Target path does not exist: ${target}`);
432
+ return 2;
433
+ }
434
+ const storeDir = str(args, 'store');
435
+ const store = storeDir ? new Store(storeDir) : undefined;
436
+ const candidateCapability = loadEvolveCandidate(args);
437
+ const cases = loadEvolveCases(args, candidateCapability);
438
+ const learnReportPath = loadEvolveLearn(args);
439
+ const proposeFromGaps = bool(args, 'propose') && !candidateCapability && !learnReportPath;
440
+ const allGaps = bool(args, 'all-gaps');
441
+ const result = runEvolutionCycle({
442
+ target,
443
+ rulesDir,
444
+ engineVersion: VERSION,
445
+ candidateCapability,
446
+ benchmarkCases: cases,
447
+ store,
448
+ learnReportPath,
449
+ learnMinSeverity: str(args, 'min-severity', 'MEDIUM'),
450
+ proposeFromGaps,
451
+ allGaps,
452
+ onUnproposable: (gap, reason) => {
453
+ console.error(`warning: ${gap.id} not proposable (${reason})`);
454
+ },
455
+ });
456
+ printEvolutionResult(result);
457
+ return 0;
458
+ }
459
+ /** Resolve `--learn <report.md>` to an existing path, or undefined. */
460
+ function loadEvolveLearn(args) {
461
+ const reportPath = str(args, 'learn');
462
+ if (!reportPath)
463
+ return undefined;
464
+ if (!fs.existsSync(reportPath)) {
465
+ console.error(`warning: learn report not found (${reportPath}) — ignoring --learn`);
466
+ return undefined;
467
+ }
468
+ return reportPath;
469
+ }
470
+ function loadEvolveCandidate(args) {
471
+ const candidateFile = str(args, 'candidate');
472
+ if (!candidateFile)
473
+ return undefined;
474
+ const { pack, warnings } = loadPackFile(candidateFile);
475
+ for (const w of warnings)
476
+ console.error(`warning: ${w}`);
477
+ if (!pack) {
478
+ console.error(`Could not load candidate pack: ${candidateFile}`);
479
+ return undefined;
480
+ }
481
+ return capabilityFromPack(pack, { createdBy: 'cli' });
482
+ }
483
+ function loadEvolveCases(args, candidateCapability) {
484
+ const benchDir = str(args, 'bench-dir');
485
+ const cases = benchDir ? loadBenchCases(benchDir) : [];
486
+ if (candidateCapability && cases.length === 0) {
487
+ console.error('warning: no benchmark cases supplied (--bench-dir). The release decision will be vacuous.');
488
+ }
489
+ return cases;
490
+ }
491
+ function loadBenchCases(dir) {
492
+ if (!fs.existsSync(dir))
493
+ return [];
494
+ const cases = [];
495
+ for (const entry of fs.readdirSync(dir)) {
496
+ if (!entry.endsWith('.json'))
497
+ continue;
498
+ try {
499
+ const raw = JSON.parse(fs.readFileSync(path.join(dir, entry), 'utf8'));
500
+ if (raw && raw.id && Array.isArray(raw.expected) && raw.fixture)
501
+ cases.push(raw);
502
+ }
503
+ catch {
504
+ console.error(`warning: could not read benchmark case ${entry}`);
505
+ }
506
+ }
507
+ return cases;
508
+ }
509
+ function printEvolutionResult(result) {
510
+ console.log(`# Evolution: ${result.snapshot.root}`);
511
+ console.log();
512
+ console.log(`snapshot : ${result.snapshot.id}`);
513
+ printCoverage('before', result.before.coverage);
514
+ printGaps(result.before.gaps);
515
+ if (!result.candidate) {
516
+ console.log();
517
+ if (result.proposed.length > 0) {
518
+ console.log(`proposed : ${result.proposed.length} candidate(s) (none benchmarked)`);
519
+ printProposed(result.proposed);
520
+ }
521
+ else {
522
+ console.log('No candidate capability supplied (--candidate <pack.yaml>).');
523
+ console.log('This was the deterministic BEFORE half of the loop only.');
524
+ console.log('Pass --propose to auto-propose a candidate from the gaps.');
525
+ }
526
+ printQueue(result.queue);
527
+ printSchedule(result.schedule);
528
+ return;
529
+ }
530
+ const release = result.candidate.release;
531
+ console.log();
532
+ console.log(`candidate : ${result.candidate.capabilityId} (gaps: ${result.candidate.gapIds.join(', ')})`);
533
+ if (result.proposed.length > 0)
534
+ printProposed(result.proposed);
535
+ if (result.learnedSuggestions !== undefined) {
536
+ console.log(`learned : ${result.learnedSuggestions} suggestion(s) from report`);
537
+ }
538
+ if (release) {
539
+ console.log(`release decision: ${release.decision}`);
540
+ console.log(` precision ${release.precision.toFixed(3)} · recall ${release.recall.toFixed(3)} · regressions ${release.regressions}`);
541
+ for (const r of release.reasons)
542
+ console.log(` - ${r}`);
543
+ }
544
+ if (result.after) {
545
+ console.log();
546
+ printCoverage('after', result.after.coverage);
547
+ const d = result.after.delta;
548
+ console.log(`delta : coverage +${d.coverageDelta}pt · ${d.findingsAdded.length} finding(s) added · gap reduced: ${d.gapReduced}`);
549
+ if (d.findingsAdded.length)
550
+ console.log(` new findings: ${d.findingsAdded.join(', ')}`);
551
+ }
552
+ if (result.queue) {
553
+ console.log(`queue : ${result.queue.open} open · ${result.queue.closed} closed · ${result.queue.total} total`);
554
+ }
555
+ printSchedule(result.schedule);
556
+ }
557
+ function printSchedule(schedule) {
558
+ if (!schedule)
559
+ return;
560
+ for (const c of schedule.candidates) {
561
+ const blocked = c.blockedReason ? ` · blocked: ${c.blockedReason}` : '';
562
+ console.log(`schedule : ${c.capabilityId} → ${c.outcome} ` +
563
+ `(precision ${c.release.precision.toFixed(3)} · recall ${c.release.recall.toFixed(3)} · ` +
564
+ `regressions ${c.release.regressions}, closed: ${c.closedGapIds.length} gap(s))${blocked}`);
565
+ }
566
+ console.log(`schedule : ${schedule.attempted} attempted · ${schedule.released} released · ${schedule.rejected} rejected`);
567
+ }
568
+ function printQueue(queue) {
569
+ if (!queue)
570
+ return;
571
+ console.log(`queue : ${queue.open} open · ${queue.closed} closed · ${queue.total} total`);
572
+ }
573
+ function printCoverage(label, cov) {
574
+ console.log(`${label.padEnd(16)}: language coverage ${cov.languageCoverage}% · automation ${cov.automationCoverage}%`);
575
+ if (cov.unsupportedLanguages.length) {
576
+ console.log(` unsupported : ${cov.unsupportedLanguages.join(', ')}`);
577
+ }
578
+ }
579
+ function printGaps(gaps) {
580
+ if (gaps.length === 0) {
581
+ console.log('gaps : none');
582
+ return;
583
+ }
584
+ console.log(`gaps : ${gaps.length}`);
585
+ for (const g of gaps)
586
+ console.log(` - ${g.id} [${g.priority}] ${g.requiredCapability}`);
587
+ }
588
+ function printProposed(proposed) {
589
+ for (const c of proposed) {
590
+ const langs = c.capability.languages?.join(', ') ?? 'unknown';
591
+ const rules = c.capability.pack?.rules.length ?? 0;
592
+ console.log(` proposed : ${c.id} (${rules} rule(s), langs: ${langs}) → gap ${c.gapIds.join(', ')} [${c.status}]`);
593
+ }
594
+ console.log(' review required — proposals are unreviewed bootstrap packs, never registered.');
595
+ }
596
+ /* ------------------------------------------------------------------ utils -- */
597
+ function readVersion() {
598
+ const candidates = [
599
+ path.resolve(HERE, '..', 'package.json'),
600
+ path.resolve(HERE, '..', '..', 'package.json'),
601
+ ];
602
+ for (const c of candidates) {
603
+ try {
604
+ const pkg = JSON.parse(fs.readFileSync(c, 'utf8'));
605
+ if (pkg.version)
606
+ return pkg.version;
607
+ }
608
+ catch {
609
+ /* fall through */
610
+ }
611
+ }
612
+ return '0.0.0';
613
+ }
614
+ const HELP = `
615
+ usa — Universal Software Auditor
616
+
617
+ usa audit [path] Audit a project and write a Markdown report
618
+ usa detect [path] Print the auto-detected facts and maturity
619
+ usa rules [--section S2] List all loaded rule packs and rules
620
+ usa explain <RULE-ID> Show everything about one rule
621
+ usa diff <before> <after> Compare two previously generated reports
622
+ usa init [path] Scaffold .usa.yaml + a GitHub Actions workflow
623
+ usa bootstrap [path] Propose rule packs for stacks USA cannot audit yet
624
+ usa learn <report.md> Generate suggested rules from audit findings
625
+ usa evolve [path] Run the audit → gap → candidate → release loop
626
+
627
+ evolve options
628
+ --store <dir> Persist audit runs/results (content-addressed store)
629
+ --candidate <file> A candidate capability pack (YAML) to benchmark and release
630
+ --propose Auto-propose a candidate from the gaps (bootstrap catalog)
631
+ --all-gaps Evaluate every proposable candidate (one per open gap)
632
+ --learn <report.md> Propose a candidate from a report's open findings (usa learn)
633
+ --min-severity <s> Min severity for --learn (default MEDIUM)
634
+ --bench-dir <dir> Directory of benchmark case *.json files
635
+ --rules-dir <dir> Rule pack directory (default bundled rules/)
636
+
637
+ learn options
638
+ --out <file> Output YAML file (default learn-suggestions.yaml)
639
+ --min-severity <s> Minimum severity to consider (CRITICAL|HIGH|MEDIUM|LOW|FUTURE, default MEDIUM)
640
+
641
+ audit options
642
+ --out <file> Report path (default AUDIT.md)
643
+ --depth <level> quick | standard | deep (default standard)
644
+ --profile <stage> auto | prototype | mvp | beta | production | legacy
645
+ --rules-dir <dir> Rule pack directory (default bundled rules/)
646
+ --config <file> Explicit .usa.yaml location
647
+ --include <packs> Force these packs on (comma separated)
648
+ --exclude <packs> Force these packs off
649
+ --fact <ns:value> Assert a fact detection missed, e.g. --fact has:database
650
+ --allow-commands Run \`command:\` checks (shells out; off by default)
651
+ --fail-on <sev> Exit 1 on findings >= sev: critical|high|medium|low|none
652
+ --quiet Only errors
653
+ --max-files <n> Index at most n files (overrides config; default 60000)
654
+ --max-bytes <n> Skip files larger than n bytes (overrides config; default 2 MiB)
655
+
656
+ bootstrap options
657
+ --out <file|dir> Write pack files instead of printing (default: print)
658
+
659
+ examples
660
+ usa audit . --depth deep
661
+ usa bootstrap ~/code/legacy-php-app --out /tmp/packs
662
+ usa audit ../api --profile production --fail-on high
663
+ usa audit . --out reports/audit-$(date +%F).md
664
+ usa learn AUDIT.md --out swift-suggestions.yaml
665
+ `.trim();
666
+ const WORKFLOW_TEMPLATE = `# USA — Universal Software Auditor
667
+ # Runs on every PR and pushes a Markdown summary you can read in the Actions UI.
668
+ name: USA Audit
669
+
670
+ on:
671
+ pull_request:
672
+ push:
673
+ branches: [master]
674
+ workflow_dispatch:
675
+ inputs:
676
+ depth:
677
+ description: Audit depth
678
+ required: false
679
+ default: standard
680
+ type: choice
681
+ options: [quick, standard, deep]
682
+
683
+ permissions:
684
+ contents: read
685
+ `;
686
+ // ---- main entry point ----
687
+ // Run only when this file is the entry point. Comparing `import.meta.url` to
688
+ // `file://${process.argv[1]}` breaks under npm's bin symlinks: argv[1] is the
689
+ // `.bin/usa` symlink while import.meta.url is the real dist/cli.js path, so
690
+ // the guard never matches and the CLI exits silently. Resolve argv[1] to its
691
+ // real path first (and guard the fs call so a non-file argv never throws).
692
+ function isEntryPoint(argv1 = process.argv[1], thisUrl = import.meta.url) {
693
+ if (!argv1)
694
+ return false;
695
+ try {
696
+ return thisUrl === pathToFileURL(fs.realpathSync(argv1)).href;
697
+ }
698
+ catch {
699
+ return false;
700
+ }
701
+ }
702
+ export { isEntryPoint };
703
+ if (isEntryPoint()) {
704
+ process.exit(main(process.argv.slice(2)));
705
+ }
706
+ //# sourceMappingURL=cli.js.map