vibecodedshit 0.0.0-stage → 0.3.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 (236) hide show
  1. package/LICENSE +95 -0
  2. package/README.md +73 -2
  3. package/assets/fonts/NotoEmoji-OFL.txt +93 -0
  4. package/assets/fonts/NotoEmoji-Poop.ttf +0 -0
  5. package/assets/fonts/SpaceGrotesk-Bold.ttf +0 -0
  6. package/assets/fonts/SpaceGrotesk-OFL.txt +93 -0
  7. package/dist/bin.js +11 -0
  8. package/dist/card/previous.js +38 -0
  9. package/dist/card/render.js +71 -0
  10. package/dist/cli.js +138 -0
  11. package/dist/commands/card.js +69 -0
  12. package/dist/commands/common.js +1 -0
  13. package/dist/commands/diff.js +54 -0
  14. package/dist/commands/footer.js +28 -0
  15. package/dist/commands/report.js +49 -0
  16. package/dist/commands/route.js +62 -0
  17. package/dist/commands/submit.js +125 -0
  18. package/dist/commands/summary.js +77 -0
  19. package/dist/diff/compare.js +73 -0
  20. package/dist/diff/conditions.js +87 -0
  21. package/dist/diff/format.js +23 -0
  22. package/dist/exec/docker.js +158 -0
  23. package/dist/exec/findings.js +73 -0
  24. package/dist/exec/plan.js +50 -0
  25. package/dist/exec/reports.js +90 -0
  26. package/dist/exec/roots.js +49 -0
  27. package/dist/exec/run.js +182 -0
  28. package/dist/exec/steps/build.js +45 -0
  29. package/dist/exec/steps/coverage-install.js +12 -0
  30. package/dist/exec/steps/install.js +41 -0
  31. package/dist/exec/steps/knip.js +28 -0
  32. package/dist/exec/steps/lint.js +25 -0
  33. package/dist/exec/steps/lockfiles.js +25 -0
  34. package/dist/exec/steps/test.js +17 -0
  35. package/dist/exec/steps/typecheck.js +40 -0
  36. package/dist/exec/test-reports/coverage-py.js +23 -0
  37. package/dist/exec/test-reports/coverage-ranges.js +65 -0
  38. package/dist/exec/test-reports/coverage-summary.js +23 -0
  39. package/dist/exec/test-reports/jest-json.js +31 -0
  40. package/dist/exec/test-reports/junit.js +24 -0
  41. package/dist/exec/test-reports/mocha-json.js +18 -0
  42. package/dist/exec/test-reports/vitest-json.js +5 -0
  43. package/dist/exec/test-runners/detect.js +99 -0
  44. package/dist/exec/test-runners/index.js +6 -0
  45. package/dist/exec/test-runners/jest.js +27 -0
  46. package/dist/exec/test-runners/mocha.js +28 -0
  47. package/dist/exec/test-runners/node-test.js +24 -0
  48. package/dist/exec/test-runners/pytest.js +18 -0
  49. package/dist/exec/test-runners/script.js +48 -0
  50. package/dist/exec/test-runners/spec.js +1 -0
  51. package/dist/exec/test-runners/vitest.js +28 -0
  52. package/dist/exec/tests.js +120 -0
  53. package/dist/exec/types.js +1 -0
  54. package/dist/interactive/after-scan.js +121 -0
  55. package/dist/interactive/docker-setup.js +97 -0
  56. package/dist/interactive/prompt.js +17 -0
  57. package/dist/interactive/system.js +10 -0
  58. package/dist/manifest/package-json.js +36 -0
  59. package/dist/manifest/python.js +106 -0
  60. package/dist/measure/d3/index.js +45 -0
  61. package/dist/measure/d3/presets.js +100 -0
  62. package/dist/measure/d3/py/adapter.js +67 -0
  63. package/dist/measure/d3/py/config.js +80 -0
  64. package/dist/measure/d3/py/contracts.js +57 -0
  65. package/dist/measure/d3/py/domain.js +57 -0
  66. package/dist/measure/d3/py/graph.js +214 -0
  67. package/dist/measure/d3/py/interpreter.js +31 -0
  68. package/dist/measure/d3/py/layers.js +120 -0
  69. package/dist/measure/d3/py/roots.js +58 -0
  70. package/dist/measure/d3/py/types.js +1 -0
  71. package/dist/measure/d3/scc.js +64 -0
  72. package/dist/measure/d3/ts/adapter.js +371 -0
  73. package/dist/measure/d3/ts/classify.js +107 -0
  74. package/dist/measure/d3/ts/config.js +318 -0
  75. package/dist/measure/d3/ts/domain.js +23 -0
  76. package/dist/measure/d3/ts/rules.js +65 -0
  77. package/dist/measure/d3/types.js +5 -0
  78. package/dist/measure/deps/cvss.js +66 -0
  79. package/dist/measure/deps/declared.js +195 -0
  80. package/dist/measure/deps/files.js +40 -0
  81. package/dist/measure/deps/lockfiles.js +308 -0
  82. package/dist/measure/deps/osv.js +122 -0
  83. package/dist/measure/deps/registry.js +142 -0
  84. package/dist/measure/deps/resolved.js +78 -0
  85. package/dist/measure/deps/units.js +5 -0
  86. package/dist/measure/eslint/health-plugin.js +19 -0
  87. package/dist/measure/ops/docs-commands.js +73 -0
  88. package/dist/measure/ops/docs-resolve.js +217 -0
  89. package/dist/measure/ops/entry-points.js +49 -0
  90. package/dist/measure/ops/env-example.js +20 -0
  91. package/dist/measure/ops/env-reads.js +46 -0
  92. package/dist/measure/ops/runtime.js +73 -0
  93. package/dist/measure/p1-1.js +8 -0
  94. package/dist/measure/p1-2.js +8 -0
  95. package/dist/measure/p1-3.js +8 -0
  96. package/dist/measure/p1-4.js +41 -0
  97. package/dist/measure/p1-5.js +59 -0
  98. package/dist/measure/p2-1.js +8 -0
  99. package/dist/measure/p2-2.js +44 -0
  100. package/dist/measure/p2-3.js +81 -0
  101. package/dist/measure/p2-4.js +288 -0
  102. package/dist/measure/p2-5.js +8 -0
  103. package/dist/measure/p3-1.js +16 -0
  104. package/dist/measure/p3-2.js +26 -0
  105. package/dist/measure/p3-3.js +37 -0
  106. package/dist/measure/p3-4.js +26 -0
  107. package/dist/measure/p3-5.js +26 -0
  108. package/dist/measure/p4-1.js +184 -0
  109. package/dist/measure/p4-2.js +27 -0
  110. package/dist/measure/p4-3.js +106 -0
  111. package/dist/measure/p4-4.js +309 -0
  112. package/dist/measure/p5-1.js +117 -0
  113. package/dist/measure/p5-2.js +80 -0
  114. package/dist/measure/p5-3.js +21 -0
  115. package/dist/measure/p5-4.js +161 -0
  116. package/dist/measure/p5-5.js +158 -0
  117. package/dist/measure/p6-1.js +20 -0
  118. package/dist/measure/p6-2.js +20 -0
  119. package/dist/measure/p6-3.js +40 -0
  120. package/dist/measure/p6-4.js +31 -0
  121. package/dist/measure/p6-5.js +60 -0
  122. package/dist/measure/p6-6.js +54 -0
  123. package/dist/measure/py/typecheck-config.js +95 -0
  124. package/dist/measure/registry.js +31 -0
  125. package/dist/measure/safety/py-tests.js +124 -0
  126. package/dist/measure/safety/ts-barrels.js +68 -0
  127. package/dist/measure/safety/ts-cases.js +111 -0
  128. package/dist/measure/safety/ts-credits.js +101 -0
  129. package/dist/measure/safety/ts-imports.js +63 -0
  130. package/dist/measure/safety/ts-resolve.js +43 -0
  131. package/dist/measure/ts/directives.js +37 -0
  132. package/dist/measure/ts/exports.js +60 -0
  133. package/dist/measure/ts/tsconfig.js +179 -0
  134. package/dist/measure/types.js +1 -0
  135. package/dist/measure/units.js +41 -0
  136. package/dist/methodology/load.js +159 -0
  137. package/dist/methodology/schema.js +105 -0
  138. package/dist/python/deadcode.js +27 -0
  139. package/dist/python/env.js +13 -0
  140. package/dist/python/helper.js +24 -0
  141. package/dist/python/imports.js +40 -0
  142. package/dist/python/nox.js +16 -0
  143. package/dist/python/scope.js +35 -0
  144. package/dist/python/tests.js +19 -0
  145. package/dist/python/typing.js +27 -0
  146. package/dist/report/escape.js +5 -0
  147. package/dist/report/format.js +75 -0
  148. package/dist/report/render.js +213 -0
  149. package/dist/report/roadmap.js +58 -0
  150. package/dist/report/scares.js +83 -0
  151. package/dist/report/style.js +27 -0
  152. package/dist/report-only/hotspots.js +48 -0
  153. package/dist/route/check.js +37 -0
  154. package/dist/route/files.js +74 -0
  155. package/dist/route/order.js +22 -0
  156. package/dist/route/select.js +114 -0
  157. package/dist/route/write.js +13 -0
  158. package/dist/run/read.js +89 -0
  159. package/dist/run/recompute.js +19 -0
  160. package/dist/run/scan.js +196 -0
  161. package/dist/run/schema.js +1 -0
  162. package/dist/run/score-run.js +42 -0
  163. package/dist/run/write.js +4 -0
  164. package/dist/scope/build.js +173 -0
  165. package/dist/scope/packages.js +87 -0
  166. package/dist/scope/rules.js +21 -0
  167. package/dist/scope/tracked.js +34 -0
  168. package/dist/scope/ts-lines.js +63 -0
  169. package/dist/scope/types.js +7 -0
  170. package/dist/scoring/aggregate.js +38 -0
  171. package/dist/scoring/pool.js +65 -0
  172. package/dist/scoring/score.js +81 -0
  173. package/dist/scoring/what-if.js +38 -0
  174. package/dist/scripts/checks.js +53 -0
  175. package/dist/scripts/ci.js +139 -0
  176. package/dist/scripts/entry-points/find.js +31 -0
  177. package/dist/scripts/entry-points/index.js +18 -0
  178. package/dist/scripts/entry-points/justfile.js +62 -0
  179. package/dist/scripts/entry-points/makefile.js +66 -0
  180. package/dist/scripts/entry-points/nox.js +29 -0
  181. package/dist/scripts/entry-points/package-json.js +21 -0
  182. package/dist/scripts/entry-points/pyproject.js +73 -0
  183. package/dist/scripts/entry-points/tox.js +130 -0
  184. package/dist/scripts/expand.js +300 -0
  185. package/dist/scripts/triggers.js +151 -0
  186. package/dist/scripts/types.js +1 -0
  187. package/dist/submit/constants.js +10 -0
  188. package/dist/submit/payload.js +42 -0
  189. package/dist/submit/repo-identity.js +46 -0
  190. package/dist/submit/send.js +29 -0
  191. package/dist/tools/catalog.js +46 -0
  192. package/dist/tools/image.js +180 -0
  193. package/dist/tools/kit.js +71 -0
  194. package/dist/tools/knip.js +169 -0
  195. package/dist/tools/outcome.js +21 -0
  196. package/dist/tools/resolve.js +66 -0
  197. package/dist/tools/workcopy.js +27 -0
  198. package/dist/util/ini.js +37 -0
  199. package/dist/util/pep440.js +163 -0
  200. package/dist/util/toml.js +27 -0
  201. package/docker/Dockerfile +142 -0
  202. package/docker/bin/health-capture +5 -0
  203. package/docker/bin/health-coverage-install +5 -0
  204. package/docker/bin/health-eslint +66 -0
  205. package/docker/bin/health-pytest +10 -0
  206. package/docker/bin/health-tsc +10 -0
  207. package/docker/bin/health-venv +11 -0
  208. package/docker/config/eslint.config.mjs +4 -0
  209. package/docker/config/eslint.rules.mjs +65 -0
  210. package/docker/config/eslint.untyped.config.mjs +4 -0
  211. package/docker/config/knip.json +1 -0
  212. package/docker/config/ruff.toml +7 -0
  213. package/methodology/LICENSE +396 -0
  214. package/methodology/v1.0/METHODOLOGY.md +1176 -0
  215. package/methodology/v1.0/dimensions/1-safety-net.yaml +240 -0
  216. package/methodology/v1.0/dimensions/2-type-static-rigor.yaml +289 -0
  217. package/methodology/v1.0/dimensions/3-architecture-boundaries.yaml +266 -0
  218. package/methodology/v1.0/dimensions/4-complexity-maintainability.yaml +192 -0
  219. package/methodology/v1.0/dimensions/5-dependency-security-hygiene.yaml +223 -0
  220. package/methodology/v1.0/dimensions/6-operability-feedback-loop.yaml +279 -0
  221. package/methodology/v1.0/methodology.yaml +242 -0
  222. package/methodology/v1.0/presets/django.yaml +43 -0
  223. package/methodology/v1.0/presets/express.yaml +47 -0
  224. package/methodology/v1.0/presets/fastapi.yaml +52 -0
  225. package/methodology/v1.0/presets/generic.yaml +41 -0
  226. package/methodology/v1.0/presets/nextjs.yaml +50 -0
  227. package/methodology/v1.0/scope.yaml +115 -0
  228. package/package.json +61 -4
  229. package/python/dead_code_helper.py +265 -0
  230. package/python/env_helper.py +71 -0
  231. package/python/graph_helper.py +277 -0
  232. package/python/imports_helper.py +132 -0
  233. package/python/nox_helper.py +79 -0
  234. package/python/scope_helper.py +96 -0
  235. package/python/tests_helper.py +171 -0
  236. package/python/typing_helper.py +235 -0
@@ -0,0 +1,107 @@
1
+ import ts from 'typescript';
2
+ const FUNCTION_LIKE = new Set([
3
+ ts.SyntaxKind.FunctionDeclaration,
4
+ ts.SyntaxKind.FunctionExpression,
5
+ ts.SyntaxKind.ArrowFunction,
6
+ ts.SyntaxKind.MethodDeclaration,
7
+ ts.SyntaxKind.Constructor,
8
+ ts.SyntaxKind.GetAccessor,
9
+ ts.SyntaxKind.SetAccessor,
10
+ ]);
11
+ const stringOf = (node) => node !== undefined && (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) ? node.text : undefined;
12
+ /** Every import occurrence of a file, read from the TypeScript AST. */
13
+ export function classifyImports(sourceFile) {
14
+ const out = [];
15
+ const visit = (node, topLevel) => {
16
+ const inside = topLevel && !FUNCTION_LIKE.has(node.kind);
17
+ if (ts.isImportDeclaration(node)) {
18
+ const specifier = stringOf(node.moduleSpecifier);
19
+ const clause = node.importClause;
20
+ const bindings = clause?.namedBindings;
21
+ const allTypeNamed = clause !== undefined &&
22
+ clause.name === undefined &&
23
+ bindings !== undefined &&
24
+ ts.isNamedImports(bindings) &&
25
+ bindings.elements.length > 0 &&
26
+ bindings.elements.every((element) => element.isTypeOnly);
27
+ if (specifier !== undefined) {
28
+ out.push({ specifier, kind: 'static', typeOnly: clause?.isTypeOnly === true || allTypeNamed, topLevel: inside });
29
+ }
30
+ }
31
+ else if (ts.isExportDeclaration(node) && node.moduleSpecifier) {
32
+ const specifier = stringOf(node.moduleSpecifier);
33
+ const clause = node.exportClause;
34
+ const allTypeNamed = clause !== undefined && ts.isNamedExports(clause) && clause.elements.length > 0 && clause.elements.every((element) => element.isTypeOnly);
35
+ if (specifier !== undefined)
36
+ out.push({ specifier, kind: 'static', typeOnly: node.isTypeOnly || allTypeNamed, topLevel: inside });
37
+ }
38
+ else if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)) {
39
+ const specifier = stringOf(node.moduleReference.expression);
40
+ if (specifier !== undefined)
41
+ out.push({ specifier, kind: 'static', typeOnly: node.isTypeOnly, topLevel: inside });
42
+ }
43
+ else if (ts.isCallExpression(node)) {
44
+ const specifier = stringOf(node.arguments[0]);
45
+ if (specifier !== undefined) {
46
+ if (node.expression.kind === ts.SyntaxKind.ImportKeyword)
47
+ out.push({ specifier, kind: 'dynamic', typeOnly: false, topLevel: inside });
48
+ else if (ts.isIdentifier(node.expression) && node.expression.text === 'require')
49
+ out.push({ specifier, kind: 'require', typeOnly: false, topLevel: inside });
50
+ }
51
+ }
52
+ else if (ts.isImportTypeNode(node)) {
53
+ const literal = ts.isLiteralTypeNode(node.argument) ? stringOf(node.argument.literal) : undefined;
54
+ if (literal !== undefined)
55
+ out.push({ specifier: literal, kind: 'dynamic', typeOnly: true, topLevel: inside });
56
+ }
57
+ ts.forEachChild(node, (child) => visit(child, inside));
58
+ };
59
+ visit(sourceFile, true);
60
+ return out;
61
+ }
62
+ /** Package name of a specifier: `node:` removed, up to the first `/` (two segments for `@scope/pkg`). */
63
+ export function packageName(specifier) {
64
+ const bare = specifier.replace(/^node:/, '');
65
+ const parts = bare.split('/');
66
+ return bare.startsWith('@') ? parts.slice(0, 2).join('/') : (parts[0] ?? bare);
67
+ }
68
+ /** True when the file calls the global `fetch`: an unbound identifier call, or `globalThis.fetch(...)`. */
69
+ export function callsGlobalFetch(sourceFile) {
70
+ let bound = false;
71
+ let global = false;
72
+ const isFetch = (name) => name !== undefined && ts.isIdentifier(name) && name.text === 'fetch';
73
+ const visit = (node) => {
74
+ if ((ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isClassDeclaration(node) || ts.isVariableDeclaration(node) || ts.isParameter(node) || ts.isBindingElement(node) || ts.isImportSpecifier(node) || ts.isImportClause(node) || ts.isNamespaceImport(node) || ts.isImportEqualsDeclaration(node)) &&
75
+ isFetch(node.name)) {
76
+ bound = true;
77
+ }
78
+ if (ts.isCallExpression(node)) {
79
+ const callee = node.expression;
80
+ if (isFetch(callee))
81
+ global = true;
82
+ else if (ts.isPropertyAccessExpression(callee) && callee.name.text === 'fetch' && ts.isIdentifier(callee.expression) && callee.expression.text === 'globalThis') {
83
+ return void (global = true);
84
+ }
85
+ }
86
+ ts.forEachChild(node, visit);
87
+ };
88
+ visit(sourceFile);
89
+ if (!global)
90
+ return false;
91
+ if (!bound)
92
+ return true;
93
+ // A local binding only shadows the identifier form; `globalThis.fetch(...)` still counts.
94
+ let viaGlobalThis = false;
95
+ const again = (node) => {
96
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'fetch' && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === 'globalThis')
97
+ viaGlobalThis = true;
98
+ ts.forEachChild(node, again);
99
+ };
100
+ again(sourceFile);
101
+ return viaGlobalThis;
102
+ }
103
+ /** The first statement is the directive `"use client"`. */
104
+ export function hasUseClient(sourceFile) {
105
+ const first = sourceFile.statements[0];
106
+ return first !== undefined && ts.isExpressionStatement(first) && ts.isStringLiteral(first.expression) && first.expression.text === 'use client';
107
+ }
@@ -0,0 +1,318 @@
1
+ import path from 'node:path';
2
+ import ts from 'typescript';
3
+ import { readTracked, trackedPaths } from '../../../scope/tracked.js';
4
+ const CONFIG_NAMES = ['.dependency-cruiser.js', '.dependency-cruiser.cjs', '.dependency-cruiser.mjs', '.dependency-cruiser.json'];
5
+ class NonLiteral extends Error {
6
+ }
7
+ function evalLiteral(node) {
8
+ if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isNonNullExpression(node)) {
9
+ return evalLiteral(node.expression);
10
+ }
11
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
12
+ return node.text;
13
+ if (ts.isNumericLiteral(node))
14
+ return Number(node.text);
15
+ if (ts.isRegularExpressionLiteral(node))
16
+ return node.text.replace(/^\/(.*)\/[a-z]*$/s, '$1');
17
+ if (node.kind === ts.SyntaxKind.TrueKeyword)
18
+ return true;
19
+ if (node.kind === ts.SyntaxKind.FalseKeyword)
20
+ return false;
21
+ if (node.kind === ts.SyntaxKind.NullKeyword)
22
+ return null;
23
+ if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(node.operand)) {
24
+ return -Number(node.operand.text);
25
+ }
26
+ if (ts.isArrayLiteralExpression(node)) {
27
+ return node.elements.map((element) => {
28
+ if (ts.isSpreadElement(element))
29
+ throw new NonLiteral('spread element');
30
+ return evalLiteral(element);
31
+ });
32
+ }
33
+ if (ts.isObjectLiteralExpression(node)) {
34
+ const out = {};
35
+ for (const property of node.properties) {
36
+ if (!ts.isPropertyAssignment(property))
37
+ throw new NonLiteral('non-literal property');
38
+ const name = property.name;
39
+ const key = ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name) ? name.text : undefined;
40
+ if (key === undefined)
41
+ throw new NonLiteral('computed property name');
42
+ out[key] = evalLiteral(property.initializer);
43
+ }
44
+ return out;
45
+ }
46
+ throw new NonLiteral(`${ts.SyntaxKind[node.kind]} is not a literal`);
47
+ }
48
+ /** The object literal a JS config exports: `module.exports = {...}`, `export default {...}` or a top-level const they name. */
49
+ function exportedObject(sourceFile) {
50
+ const consts = new Map();
51
+ for (const statement of sourceFile.statements) {
52
+ if (!ts.isVariableStatement(statement))
53
+ continue;
54
+ for (const declaration of statement.declarationList.declarations) {
55
+ if (ts.isIdentifier(declaration.name) && declaration.initializer)
56
+ consts.set(declaration.name.text, declaration.initializer);
57
+ }
58
+ }
59
+ const resolve = (expression) => {
60
+ let current = expression;
61
+ while (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isSatisfiesExpression(current))
62
+ current = current.expression;
63
+ if (ts.isObjectLiteralExpression(current))
64
+ return current;
65
+ if (ts.isIdentifier(current)) {
66
+ const target = consts.get(current.text);
67
+ return target ? resolve(target) : undefined;
68
+ }
69
+ return undefined;
70
+ };
71
+ for (const statement of sourceFile.statements) {
72
+ if (ts.isExportAssignment(statement))
73
+ return resolve(statement.expression);
74
+ if (ts.isExpressionStatement(statement) &&
75
+ ts.isBinaryExpression(statement.expression) &&
76
+ statement.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
77
+ ts.isPropertyAccessExpression(statement.expression.left) &&
78
+ ts.isIdentifier(statement.expression.left.expression) &&
79
+ statement.expression.left.expression.text === 'module' &&
80
+ statement.expression.left.name.text === 'exports') {
81
+ return resolve(statement.expression.right);
82
+ }
83
+ }
84
+ return undefined;
85
+ }
86
+ /** Elements of an array-literal property, one literal evaluation per element. */
87
+ function elementsOf(object, key) {
88
+ for (const property of object.properties) {
89
+ if (!ts.isPropertyAssignment(property))
90
+ continue;
91
+ const name = property.name;
92
+ if (!(ts.isIdentifier(name) || ts.isStringLiteral(name)) || name.text !== key)
93
+ continue;
94
+ let init = property.initializer;
95
+ while (ts.isParenthesizedExpression(init) || ts.isAsExpression(init))
96
+ init = init.expression;
97
+ if (!ts.isArrayLiteralExpression(init))
98
+ return [{ node: init, error: `${key} is not an array literal` }];
99
+ return init.elements.map((node) => {
100
+ try {
101
+ if (ts.isSpreadElement(node))
102
+ throw new NonLiteral('spread element');
103
+ return { node, value: evalLiteral(node) };
104
+ }
105
+ catch (err) {
106
+ const name = ts.isObjectLiteralExpression(node) ? scalar(node, 'name') : undefined;
107
+ return { node, error: err instanceof NonLiteral ? err.message : String(err), ...(typeof name === 'string' ? { name } : {}) };
108
+ }
109
+ });
110
+ }
111
+ return undefined;
112
+ }
113
+ function scalar(object, key) {
114
+ for (const property of object.properties) {
115
+ if (ts.isPropertyAssignment(property) && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) && property.name.text === key) {
116
+ try {
117
+ return evalLiteral(property.initializer);
118
+ }
119
+ catch {
120
+ return undefined;
121
+ }
122
+ }
123
+ }
124
+ return undefined;
125
+ }
126
+ const isObj = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
127
+ function patternList(value) {
128
+ if (typeof value === 'string')
129
+ return [value];
130
+ if (Array.isArray(value) && value.every((v) => typeof v === 'string'))
131
+ return value;
132
+ return undefined;
133
+ }
134
+ function pathSpec(value, label) {
135
+ if (value === undefined)
136
+ return {};
137
+ if (!isObj(value))
138
+ throw new NonLiteral(`${label} is not an object`);
139
+ const spec = {};
140
+ for (const [key, entry] of Object.entries(value)) {
141
+ if (key !== 'path' && key !== 'pathNot')
142
+ throw new NonLiteral(`${label}.${key} needs an installed project`);
143
+ const list = patternList(entry);
144
+ if (!list)
145
+ throw new NonLiteral(`${label}.${key} is not a string or list of strings`);
146
+ for (const pattern of list)
147
+ new RegExp(pattern);
148
+ spec[key] = entry;
149
+ }
150
+ return spec;
151
+ }
152
+ const LITERAL = '(?:[\\w@-]|\\\\\\.)+';
153
+ const unescapeLiteral = (text) => text.replace(/\\\./g, '.').replace(/\\\//g, '/');
154
+ /** The directories a regex pattern names, or undefined when it does not name any. */
155
+ export function directoriesOf(pattern) {
156
+ const anywhere = new RegExp(`\\(\\^\\|\\\\?/\\)(${LITERAL})(?:\\\\?/)`).exec(pattern);
157
+ if (anywhere?.[1])
158
+ return [unescapeLiteral(anywhere[1])];
159
+ if (!pattern.startsWith('^'))
160
+ return undefined;
161
+ const group = new RegExp(`^\\^\\(((?:${LITERAL}(?:\\/${LITERAL})*)(?:\\|(?:${LITERAL}(?:\\/${LITERAL})*))*)\\)((?:\\/${LITERAL})*)(?:\\/|$)`).exec(pattern);
162
+ if (group?.[1])
163
+ return group[1].split('|').map((alt) => unescapeLiteral(`${alt}${group[2] ?? ''}`));
164
+ const literal = new RegExp(`^\\^(${LITERAL}(?:\\/${LITERAL})*)(?:\\/|$)`).exec(pattern);
165
+ if (literal?.[1])
166
+ return [unescapeLiteral(literal[1])];
167
+ return undefined;
168
+ }
169
+ function directorySet(value) {
170
+ const set = new Set();
171
+ for (const pattern of value === undefined ? [] : Array.isArray(value) ? value : [value]) {
172
+ for (const dir of directoriesOf(pattern) ?? [])
173
+ set.add(dir);
174
+ }
175
+ return set;
176
+ }
177
+ /** A direction rule: `from.path` and `to.path` both name directories and the two directory sets differ. */
178
+ export function isDirectionRule(rule) {
179
+ const from = directorySet(rule.from.path);
180
+ const to = directorySet(rule.to.path);
181
+ if (from.size === 0 || to.size === 0)
182
+ return false;
183
+ return from.size !== to.size || [...from].some((dir) => !to.has(dir));
184
+ }
185
+ function rawPathSpec(value) {
186
+ if (!isObj(value))
187
+ return {};
188
+ const spec = {};
189
+ for (const key of ['path', 'pathNot']) {
190
+ const list = patternList(value[key]);
191
+ if (list)
192
+ spec[key] = value[key];
193
+ }
194
+ return spec;
195
+ }
196
+ const RULE_KEYS = new Set(['name', 'severity', 'comment', 'from', 'to']);
197
+ /** Reads one rules file without executing it. Undefined when the file is absent or cannot be read. */
198
+ export function readDeclaredConfig(repoPath, file, warnings = []) {
199
+ const text = readTracked(repoPath, file);
200
+ if (text === null)
201
+ return undefined;
202
+ const config = { file, forbidden: [], allowed: undefined, allowedSeverity: 'warn', declared: false };
203
+ let forbidden;
204
+ let allowed;
205
+ let allowedSeverity;
206
+ try {
207
+ if (file.endsWith('.json')) {
208
+ const doc = JSON.parse(text);
209
+ if (!isObj(doc))
210
+ throw new Error('not an object');
211
+ const wrap = (value) => (Array.isArray(value) ? value.map((v) => ({ value: v })) : undefined);
212
+ forbidden = wrap(doc.forbidden);
213
+ allowed = wrap(doc.allowed);
214
+ allowedSeverity = doc.allowedSeverity;
215
+ if (doc.options !== undefined || doc.extends !== undefined)
216
+ warnings.push(`${file}: "options" and "extends" are ignored`);
217
+ }
218
+ else {
219
+ const sourceFile = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
220
+ const object = exportedObject(sourceFile);
221
+ if (!object)
222
+ throw new Error('no module.exports or export default object literal');
223
+ forbidden = elementsOf(object, 'forbidden');
224
+ allowed = elementsOf(object, 'allowed');
225
+ allowedSeverity = scalar(object, 'allowedSeverity');
226
+ if (object.properties.some((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && (p.name.text === 'options' || p.name.text === 'extends'))) {
227
+ warnings.push(`${file}: "options" and "extends" are ignored`);
228
+ }
229
+ }
230
+ }
231
+ catch (err) {
232
+ warnings.push(`${file}: could not be read, treated as absent (${err instanceof Error ? err.message : String(err)})`);
233
+ return undefined;
234
+ }
235
+ const ruleName = (value, index, known) => known ?? (isObj(value) && typeof value.name === 'string' ? value.name : `#${index + 1}`);
236
+ (forbidden ?? []).forEach((entry, index) => {
237
+ if (entry.error !== undefined || !isObj(entry.value)) {
238
+ warnings.push(`${file}: forbidden rule ${ruleName(entry.value, index, entry.name)} skipped (${entry.error ?? 'not an object'})`);
239
+ return;
240
+ }
241
+ const rule = entry.value;
242
+ const raw = { from: rawPathSpec(rule.from), to: rawPathSpec(rule.to) };
243
+ if (isDirectionRule(raw))
244
+ config.declared = true;
245
+ try {
246
+ const extra = Object.keys(rule).find((key) => !RULE_KEYS.has(key));
247
+ if (extra !== undefined)
248
+ throw new NonLiteral(`rule key ${extra} needs an installed project`);
249
+ if (typeof rule.name !== 'string')
250
+ throw new NonLiteral('rule has no name');
251
+ const severity = rule.severity;
252
+ if (severity !== 'error' && severity !== 'warn' && severity !== 'info' && severity !== 'ignore')
253
+ throw new NonLiteral('unknown severity');
254
+ const from = pathSpec(rule.from, 'from');
255
+ const to = pathSpec(rule.to, 'to');
256
+ if (severity === 'error' || severity === 'warn')
257
+ config.forbidden.push({ name: rule.name, severity, from, to });
258
+ }
259
+ catch (err) {
260
+ warnings.push(`${file}: forbidden rule ${ruleName(rule, index)} skipped (${err instanceof Error ? err.message : String(err)})`);
261
+ }
262
+ });
263
+ if (allowed) {
264
+ const rules = [];
265
+ let complete = true;
266
+ allowed.forEach((entry, index) => {
267
+ if (entry.error !== undefined || !isObj(entry.value)) {
268
+ warnings.push(`${file}: allowed rule ${ruleName(entry.value, index, entry.name)} skipped (${entry.error ?? 'not an object'})`);
269
+ complete = false;
270
+ return;
271
+ }
272
+ const rule = entry.value;
273
+ const raw = { from: rawPathSpec(rule.from), to: rawPathSpec(rule.to) };
274
+ if (isDirectionRule(raw))
275
+ config.declared = true;
276
+ try {
277
+ const extra = Object.keys(rule).find((key) => !RULE_KEYS.has(key));
278
+ if (extra !== undefined)
279
+ throw new NonLiteral(`rule key ${extra} needs an installed project`);
280
+ rules.push({ from: pathSpec(rule.from, 'from'), to: pathSpec(rule.to, 'to') });
281
+ }
282
+ catch (err) {
283
+ warnings.push(`${file}: allowed rule ${ruleName(rule, index)} skipped (${err instanceof Error ? err.message : String(err)})`);
284
+ complete = false;
285
+ }
286
+ });
287
+ if (!complete)
288
+ warnings.push(`${file}: the whole "allowed" list is dropped because one of its rules was skipped`);
289
+ const severity = allowedSeverity === undefined ? 'warn' : allowedSeverity;
290
+ if (severity === 'error' || severity === 'warn')
291
+ config.allowedSeverity = severity;
292
+ if (complete && rules.length > 0 && (severity === 'error' || severity === 'warn'))
293
+ config.allowed = rules;
294
+ }
295
+ return config;
296
+ }
297
+ /**
298
+ * The nearest rules file from `packageDir` upwards that declares at least one direction rule.
299
+ * Files that cannot be read, or that declare nothing, are passed over.
300
+ */
301
+ export function findDeclaredConfig(repoPath, packageDir, warnings, cache) {
302
+ const tracked = new Set(trackedPaths(repoPath));
303
+ for (let dir = path.posix.normalize(packageDir === '' ? '.' : packageDir);; dir = path.posix.dirname(dir)) {
304
+ for (const name of CONFIG_NAMES) {
305
+ const file = path.posix.join(dir, name);
306
+ if (!tracked.has(file))
307
+ continue;
308
+ if (!cache.has(file)) {
309
+ cache.set(file, readDeclaredConfig(repoPath, file, warnings));
310
+ }
311
+ const config = cache.get(file);
312
+ if (config?.declared)
313
+ return config;
314
+ }
315
+ if (dir === '.' || dir === '')
316
+ return undefined;
317
+ }
318
+ }
@@ -0,0 +1,23 @@
1
+ import path from 'node:path';
2
+ import { callsGlobalFetch, classifyImports, packageName } from './classify.js';
3
+ /** Logic files with a directory named exactly `domain` anywhere in their path. */
4
+ export function isDomainFile(file) {
5
+ return path.posix.dirname(file).split('/').includes('domain');
6
+ }
7
+ function inInfrastructure(file, directories) {
8
+ if (isDomainFile(file))
9
+ return false;
10
+ return path.posix.dirname(file).split('/').some((segment) => directories.includes(segment));
11
+ }
12
+ /**
13
+ * A domain file is impure when it imports a forbidden package (type-only included), imports an in-scope
14
+ * file under an infrastructure directory (unless that file is itself under `domain`), or calls the global `fetch`.
15
+ */
16
+ export function isImpure(sourceFile, importedFiles, lists) {
17
+ const forbidden = new Set(lists.forbiddenImports.typescript_javascript);
18
+ if (classifyImports(sourceFile).some((occurrence) => forbidden.has(packageName(occurrence.specifier))))
19
+ return true;
20
+ if (importedFiles.some((file) => inInfrastructure(file, lists.infrastructureDirectories)))
21
+ return true;
22
+ return callsGlobalFetch(sourceFile);
23
+ }
@@ -0,0 +1,65 @@
1
+ const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2
+ function emit(node) {
3
+ const alternatives = [];
4
+ for (const [segment, child] of node.children) {
5
+ const rest = child.children.size > 0 ? `(?:/${emit(child)})${child.end ? '?' : ''}` : '';
6
+ alternatives.push(`${escape(segment)}${rest}`);
7
+ }
8
+ return alternatives.length === 1 ? alternatives[0] : `(?:${alternatives.join('|')})`;
9
+ }
10
+ /** An anchored regex that matches exactly the given repo-relative paths, built from a segment trie. */
11
+ export function pathsToRegex(paths) {
12
+ const root = { children: new Map(), end: false };
13
+ for (const file of paths) {
14
+ let node = root;
15
+ for (const segment of file.split('/')) {
16
+ let child = node.children.get(segment);
17
+ if (!child) {
18
+ child = { children: new Map(), end: false };
19
+ node.children.set(segment, child);
20
+ }
21
+ node = child;
22
+ }
23
+ node.end = true;
24
+ }
25
+ return `^${emit(root)}$`;
26
+ }
27
+ /** Package names as a regex that also matches an import below the package (`pkg/sub`). */
28
+ export function packagesToRegex(packages) {
29
+ return `^(?:${packages.map(escape).join('|')})(?:/|$)`;
30
+ }
31
+ /**
32
+ * Preset rules as dependency-cruiser rules for one unit. `layerFiles` maps a layer name to the
33
+ * unit's files in it; `unitFiles` is every in-scope file of the unit. One rule per (preset rule,
34
+ * `to` alternative), named `<package>:<rule>` with a `#packages` suffix on the packages alternative.
35
+ */
36
+ export function presetRules(packageDir, rules, layerFiles, unitFiles) {
37
+ const out = [];
38
+ const meta = new Map();
39
+ const union = (names) => [...new Set(names.flatMap((n) => [...(layerFiles.get(n) ?? [])]))];
40
+ for (const rule of rules) {
41
+ if (rule.severity === 'info')
42
+ continue;
43
+ let fromFiles;
44
+ if (rule.from.in)
45
+ fromFiles = union(rule.from.in);
46
+ else {
47
+ const excluded = new Set(union(rule.from.not_in ?? []));
48
+ fromFiles = unitFiles.filter((f) => !excluded.has(f));
49
+ }
50
+ if (fromFiles.length === 0)
51
+ continue;
52
+ const from = { path: pathsToRegex(fromFiles) };
53
+ const base = `${packageDir}:${rule.name}`;
54
+ const toFiles = rule.to.in ? union(rule.to.in) : [];
55
+ const add = (name, to) => {
56
+ out.push({ name, severity: rule.severity, from, to });
57
+ meta.set(name, { rule: rule.name, excludesTypeOnly: rule.type_only_imports === 'excluded' });
58
+ };
59
+ if (toFiles.length > 0)
60
+ add(base, { path: pathsToRegex(toFiles) });
61
+ if (rule.to.packages && rule.to.packages.length > 0)
62
+ add(`${base}#packages`, { path: packagesToRegex(rule.to.packages) });
63
+ }
64
+ return { rules: out, meta };
65
+ }
@@ -0,0 +1,5 @@
1
+ export const edgeKey = (from, to) => `${from}\u0000${to}`;
2
+ /** Edges of the unit that count for 3.2: importer is not a test file. */
3
+ export function internalEdges(unit) {
4
+ return unit.edges.filter((edge) => !edge.fromTest);
5
+ }
@@ -0,0 +1,66 @@
1
+ /** CVSS v3.0 and v3.1 base score from a vector string. Other versions give undefined. */
2
+ const WEIGHTS = {
3
+ AV: { N: 0.85, A: 0.62, L: 0.55, P: 0.2 },
4
+ AC: { L: 0.77, H: 0.44 },
5
+ UI: { N: 0.85, R: 0.62 },
6
+ CIA: { H: 0.56, L: 0.22, N: 0 },
7
+ };
8
+ function pr(value, changed) {
9
+ if (value === 'N')
10
+ return 0.85;
11
+ if (value === 'L')
12
+ return changed ? 0.68 : 0.62;
13
+ if (value === 'H')
14
+ return changed ? 0.5 : 0.27;
15
+ return undefined;
16
+ }
17
+ /** Smallest number, to one decimal, that is >= the input (CVSS v3.1 Roundup). */
18
+ function roundUp(input) {
19
+ const int = Math.round(input * 100000);
20
+ if (int % 10000 === 0)
21
+ return int / 100000;
22
+ return (Math.floor(int / 10000) + 1) / 10;
23
+ }
24
+ export function cvss3BaseScore(vector) {
25
+ const parts = vector.trim().split('/');
26
+ if (parts[0] !== 'CVSS:3.0' && parts[0] !== 'CVSS:3.1')
27
+ return undefined;
28
+ const metrics = new Map();
29
+ for (const part of parts.slice(1)) {
30
+ const [key, value] = part.split(':');
31
+ if (key && value)
32
+ metrics.set(key, value);
33
+ }
34
+ const get = (key) => metrics.get(key) ?? '';
35
+ const scope = get('S');
36
+ if (scope !== 'U' && scope !== 'C')
37
+ return undefined;
38
+ const changed = scope === 'C';
39
+ const av = WEIGHTS.AV[get('AV')];
40
+ const ac = WEIGHTS.AC[get('AC')];
41
+ const ui = WEIGHTS.UI[get('UI')];
42
+ const c = WEIGHTS.CIA[get('C')];
43
+ const i = WEIGHTS.CIA[get('I')];
44
+ const a = WEIGHTS.CIA[get('A')];
45
+ const priv = pr(get('PR'), changed);
46
+ if ([av, ac, ui, c, i, a, priv].some((n) => n === undefined))
47
+ return undefined;
48
+ const iss = 1 - (1 - (c ?? 0)) * (1 - (i ?? 0)) * (1 - (a ?? 0));
49
+ const impact = changed ? 7.52 * (iss - 0.029) - 3.25 * (iss - 0.02) ** 15 : 6.42 * iss;
50
+ if (impact <= 0)
51
+ return 0;
52
+ const exploitability = 8.22 * (av ?? 0) * (ac ?? 0) * (priv ?? 0) * (ui ?? 0);
53
+ const raw = changed ? Math.min(1.08 * (impact + exploitability), 10) : Math.min(impact + exploitability, 10);
54
+ return roundUp(raw);
55
+ }
56
+ export function band(score) {
57
+ if (score >= 9)
58
+ return 'critical';
59
+ if (score >= 7)
60
+ return 'high';
61
+ if (score >= 4)
62
+ return 'moderate';
63
+ if (score > 0)
64
+ return 'low';
65
+ return 'none';
66
+ }