vibecodedshit 0.0.0-stage → 0.2.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 (232) hide show
  1. package/LICENSE +95 -0
  2. package/README.md +64 -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 +8 -0
  8. package/dist/card/previous.js +38 -0
  9. package/dist/card/render.js +71 -0
  10. package/dist/cli.js +121 -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 +25 -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/manifest/package-json.js +36 -0
  55. package/dist/manifest/python.js +106 -0
  56. package/dist/measure/d3/index.js +45 -0
  57. package/dist/measure/d3/presets.js +100 -0
  58. package/dist/measure/d3/py/adapter.js +67 -0
  59. package/dist/measure/d3/py/config.js +80 -0
  60. package/dist/measure/d3/py/contracts.js +57 -0
  61. package/dist/measure/d3/py/domain.js +57 -0
  62. package/dist/measure/d3/py/graph.js +214 -0
  63. package/dist/measure/d3/py/interpreter.js +31 -0
  64. package/dist/measure/d3/py/layers.js +120 -0
  65. package/dist/measure/d3/py/roots.js +58 -0
  66. package/dist/measure/d3/py/types.js +1 -0
  67. package/dist/measure/d3/scc.js +64 -0
  68. package/dist/measure/d3/ts/adapter.js +371 -0
  69. package/dist/measure/d3/ts/classify.js +107 -0
  70. package/dist/measure/d3/ts/config.js +318 -0
  71. package/dist/measure/d3/ts/domain.js +23 -0
  72. package/dist/measure/d3/ts/rules.js +65 -0
  73. package/dist/measure/d3/types.js +5 -0
  74. package/dist/measure/deps/cvss.js +66 -0
  75. package/dist/measure/deps/declared.js +195 -0
  76. package/dist/measure/deps/files.js +40 -0
  77. package/dist/measure/deps/lockfiles.js +308 -0
  78. package/dist/measure/deps/osv.js +122 -0
  79. package/dist/measure/deps/registry.js +142 -0
  80. package/dist/measure/deps/resolved.js +78 -0
  81. package/dist/measure/deps/units.js +5 -0
  82. package/dist/measure/eslint/health-plugin.js +19 -0
  83. package/dist/measure/ops/docs-commands.js +73 -0
  84. package/dist/measure/ops/docs-resolve.js +217 -0
  85. package/dist/measure/ops/entry-points.js +49 -0
  86. package/dist/measure/ops/env-example.js +20 -0
  87. package/dist/measure/ops/env-reads.js +46 -0
  88. package/dist/measure/ops/runtime.js +73 -0
  89. package/dist/measure/p1-1.js +8 -0
  90. package/dist/measure/p1-2.js +8 -0
  91. package/dist/measure/p1-3.js +8 -0
  92. package/dist/measure/p1-4.js +41 -0
  93. package/dist/measure/p1-5.js +59 -0
  94. package/dist/measure/p2-1.js +8 -0
  95. package/dist/measure/p2-2.js +44 -0
  96. package/dist/measure/p2-3.js +81 -0
  97. package/dist/measure/p2-4.js +288 -0
  98. package/dist/measure/p2-5.js +8 -0
  99. package/dist/measure/p3-1.js +16 -0
  100. package/dist/measure/p3-2.js +26 -0
  101. package/dist/measure/p3-3.js +37 -0
  102. package/dist/measure/p3-4.js +26 -0
  103. package/dist/measure/p3-5.js +26 -0
  104. package/dist/measure/p4-1.js +184 -0
  105. package/dist/measure/p4-2.js +27 -0
  106. package/dist/measure/p4-3.js +106 -0
  107. package/dist/measure/p4-4.js +309 -0
  108. package/dist/measure/p5-1.js +117 -0
  109. package/dist/measure/p5-2.js +80 -0
  110. package/dist/measure/p5-3.js +21 -0
  111. package/dist/measure/p5-4.js +161 -0
  112. package/dist/measure/p5-5.js +158 -0
  113. package/dist/measure/p6-1.js +20 -0
  114. package/dist/measure/p6-2.js +20 -0
  115. package/dist/measure/p6-3.js +40 -0
  116. package/dist/measure/p6-4.js +31 -0
  117. package/dist/measure/p6-5.js +60 -0
  118. package/dist/measure/p6-6.js +54 -0
  119. package/dist/measure/py/typecheck-config.js +95 -0
  120. package/dist/measure/registry.js +31 -0
  121. package/dist/measure/safety/py-tests.js +124 -0
  122. package/dist/measure/safety/ts-barrels.js +68 -0
  123. package/dist/measure/safety/ts-cases.js +111 -0
  124. package/dist/measure/safety/ts-credits.js +101 -0
  125. package/dist/measure/safety/ts-imports.js +63 -0
  126. package/dist/measure/safety/ts-resolve.js +43 -0
  127. package/dist/measure/ts/directives.js +37 -0
  128. package/dist/measure/ts/exports.js +60 -0
  129. package/dist/measure/ts/tsconfig.js +179 -0
  130. package/dist/measure/types.js +1 -0
  131. package/dist/measure/units.js +41 -0
  132. package/dist/methodology/load.js +159 -0
  133. package/dist/methodology/schema.js +105 -0
  134. package/dist/python/deadcode.js +27 -0
  135. package/dist/python/env.js +13 -0
  136. package/dist/python/helper.js +24 -0
  137. package/dist/python/imports.js +40 -0
  138. package/dist/python/nox.js +16 -0
  139. package/dist/python/scope.js +35 -0
  140. package/dist/python/tests.js +19 -0
  141. package/dist/python/typing.js +27 -0
  142. package/dist/report/escape.js +5 -0
  143. package/dist/report/format.js +75 -0
  144. package/dist/report/render.js +213 -0
  145. package/dist/report/roadmap.js +58 -0
  146. package/dist/report/scares.js +83 -0
  147. package/dist/report/style.js +27 -0
  148. package/dist/report-only/hotspots.js +48 -0
  149. package/dist/route/check.js +37 -0
  150. package/dist/route/files.js +74 -0
  151. package/dist/route/order.js +22 -0
  152. package/dist/route/select.js +114 -0
  153. package/dist/route/write.js +13 -0
  154. package/dist/run/read.js +89 -0
  155. package/dist/run/recompute.js +19 -0
  156. package/dist/run/scan.js +196 -0
  157. package/dist/run/schema.js +1 -0
  158. package/dist/run/score-run.js +42 -0
  159. package/dist/run/write.js +4 -0
  160. package/dist/scope/build.js +173 -0
  161. package/dist/scope/packages.js +87 -0
  162. package/dist/scope/rules.js +21 -0
  163. package/dist/scope/tracked.js +34 -0
  164. package/dist/scope/ts-lines.js +63 -0
  165. package/dist/scope/types.js +7 -0
  166. package/dist/scoring/aggregate.js +38 -0
  167. package/dist/scoring/pool.js +65 -0
  168. package/dist/scoring/score.js +81 -0
  169. package/dist/scoring/what-if.js +38 -0
  170. package/dist/scripts/checks.js +53 -0
  171. package/dist/scripts/ci.js +139 -0
  172. package/dist/scripts/entry-points/find.js +31 -0
  173. package/dist/scripts/entry-points/index.js +18 -0
  174. package/dist/scripts/entry-points/justfile.js +62 -0
  175. package/dist/scripts/entry-points/makefile.js +66 -0
  176. package/dist/scripts/entry-points/nox.js +29 -0
  177. package/dist/scripts/entry-points/package-json.js +21 -0
  178. package/dist/scripts/entry-points/pyproject.js +73 -0
  179. package/dist/scripts/entry-points/tox.js +130 -0
  180. package/dist/scripts/expand.js +300 -0
  181. package/dist/scripts/triggers.js +151 -0
  182. package/dist/scripts/types.js +1 -0
  183. package/dist/submit/constants.js +9 -0
  184. package/dist/submit/payload.js +42 -0
  185. package/dist/submit/repo-identity.js +46 -0
  186. package/dist/submit/send.js +29 -0
  187. package/dist/tools/catalog.js +46 -0
  188. package/dist/tools/image.js +180 -0
  189. package/dist/tools/kit.js +71 -0
  190. package/dist/tools/knip.js +169 -0
  191. package/dist/tools/outcome.js +21 -0
  192. package/dist/tools/resolve.js +66 -0
  193. package/dist/tools/workcopy.js +27 -0
  194. package/dist/util/ini.js +37 -0
  195. package/dist/util/pep440.js +163 -0
  196. package/dist/util/toml.js +27 -0
  197. package/docker/Dockerfile +142 -0
  198. package/docker/bin/health-capture +5 -0
  199. package/docker/bin/health-coverage-install +5 -0
  200. package/docker/bin/health-eslint +66 -0
  201. package/docker/bin/health-pytest +10 -0
  202. package/docker/bin/health-tsc +10 -0
  203. package/docker/bin/health-venv +11 -0
  204. package/docker/config/eslint.config.mjs +4 -0
  205. package/docker/config/eslint.rules.mjs +65 -0
  206. package/docker/config/eslint.untyped.config.mjs +4 -0
  207. package/docker/config/knip.json +1 -0
  208. package/docker/config/ruff.toml +7 -0
  209. package/methodology/LICENSE +396 -0
  210. package/methodology/v1.0/METHODOLOGY.md +1176 -0
  211. package/methodology/v1.0/dimensions/1-safety-net.yaml +240 -0
  212. package/methodology/v1.0/dimensions/2-type-static-rigor.yaml +289 -0
  213. package/methodology/v1.0/dimensions/3-architecture-boundaries.yaml +266 -0
  214. package/methodology/v1.0/dimensions/4-complexity-maintainability.yaml +192 -0
  215. package/methodology/v1.0/dimensions/5-dependency-security-hygiene.yaml +223 -0
  216. package/methodology/v1.0/dimensions/6-operability-feedback-loop.yaml +279 -0
  217. package/methodology/v1.0/methodology.yaml +242 -0
  218. package/methodology/v1.0/presets/django.yaml +43 -0
  219. package/methodology/v1.0/presets/express.yaml +47 -0
  220. package/methodology/v1.0/presets/fastapi.yaml +52 -0
  221. package/methodology/v1.0/presets/generic.yaml +41 -0
  222. package/methodology/v1.0/presets/nextjs.yaml +50 -0
  223. package/methodology/v1.0/scope.yaml +115 -0
  224. package/package.json +61 -4
  225. package/python/dead_code_helper.py +265 -0
  226. package/python/env_helper.py +71 -0
  227. package/python/graph_helper.py +277 -0
  228. package/python/imports_helper.py +132 -0
  229. package/python/nox_helper.py +79 -0
  230. package/python/scope_helper.py +96 -0
  231. package/python/tests_helper.py +171 -0
  232. package/python/typing_helper.py +235 -0
@@ -0,0 +1,63 @@
1
+ import ts from 'typescript';
2
+ const isStringLiteral = (node) => node !== undefined && ts.isStringLiteralLike(node);
3
+ /**
4
+ * What a test file imports: static `import` declarations of every form (`import type` and `import { type X }` included), `export ... from`,
5
+ * `require("<literal>")` and dynamic `import("<literal>")`. `vi.mock`, `jest.mock`, `require.resolve` and non-literal paths do not count.
6
+ */
7
+ export function importFacts(sf) {
8
+ const facts = [];
9
+ const visit = (node) => {
10
+ if (ts.isImportDeclaration(node) && isStringLiteral(node.moduleSpecifier)) {
11
+ const specifier = node.moduleSpecifier.text;
12
+ const clause = node.importClause;
13
+ if (!clause)
14
+ facts.push({ specifier });
15
+ else {
16
+ if (clause.name)
17
+ facts.push({ specifier, name: 'default' });
18
+ const bindings = clause.namedBindings;
19
+ if (bindings && ts.isNamespaceImport(bindings))
20
+ facts.push({ specifier, name: '*', local: bindings.name.text });
21
+ else if (bindings)
22
+ for (const el of bindings.elements)
23
+ facts.push({ specifier, name: (el.propertyName ?? el.name).text });
24
+ if (!clause.name && !bindings)
25
+ facts.push({ specifier });
26
+ }
27
+ }
28
+ else if (ts.isExportDeclaration(node) && isStringLiteral(node.moduleSpecifier)) {
29
+ const specifier = node.moduleSpecifier.text;
30
+ const clause = node.exportClause;
31
+ if (clause && ts.isNamedExports(clause))
32
+ for (const el of clause.elements)
33
+ facts.push({ specifier, name: (el.propertyName ?? el.name).text });
34
+ else
35
+ facts.push({ specifier });
36
+ }
37
+ else if (ts.isCallExpression(node) && isStringLiteral(node.arguments[0])) {
38
+ const callee = node.expression;
39
+ if ((ts.isIdentifier(callee) && callee.text === 'require') || callee.kind === ts.SyntaxKind.ImportKeyword) {
40
+ facts.push({ specifier: node.arguments[0].text });
41
+ }
42
+ }
43
+ ts.forEachChild(node, visit);
44
+ };
45
+ visit(sf);
46
+ return facts;
47
+ }
48
+ /** Names accessed as `m.x` for each namespace-import binding `m` of a file. */
49
+ export function namespaceAccesses(sf, locals) {
50
+ const out = new Map();
51
+ if (locals.size === 0)
52
+ return out;
53
+ const visit = (node) => {
54
+ if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && locals.has(node.expression.text)) {
55
+ const set = out.get(node.expression.text) ?? new Set();
56
+ set.add(node.name.text);
57
+ out.set(node.expression.text, set);
58
+ }
59
+ ts.forEachChild(node, visit);
60
+ };
61
+ visit(sf);
62
+ return out;
63
+ }
@@ -0,0 +1,43 @@
1
+ import path from 'node:path';
2
+ import ts from 'typescript';
3
+ import { trackedPaths } from '../../scope/tracked.js';
4
+ import { compilerOptionsFor } from '../ts/tsconfig.js';
5
+ const posix = (p) => p.split(path.sep).join('/');
6
+ /**
7
+ * `ts.resolveModuleName` with the options of the nearest tsconfig and a host backed by the tracked files, so `node_modules`
8
+ * is never read and a package import resolves to nothing. NodeNext `.js` specifiers that map to `.ts` files resolve through the compiler.
9
+ */
10
+ export function createTsResolver(repoPath) {
11
+ const root = posix(path.resolve(repoPath));
12
+ const files = new Set(trackedPaths(repoPath));
13
+ const dirs = new Set(['']);
14
+ for (const file of files)
15
+ for (let d = path.posix.dirname(file); d !== '.' && !dirs.has(d); d = path.posix.dirname(d))
16
+ dirs.add(d);
17
+ const rel = (abs) => path.posix.relative(root, posix(abs));
18
+ const host = {
19
+ fileExists: (abs) => files.has(rel(abs)),
20
+ readFile: (abs) => (files.has(rel(abs)) ? ts.sys.readFile(abs) : undefined),
21
+ directoryExists: (abs) => dirs.has(rel(abs) === '.' ? '' : rel(abs)),
22
+ realpath: (abs) => abs,
23
+ getCurrentDirectory: () => root,
24
+ useCaseSensitiveFileNames: true,
25
+ };
26
+ const optionsByDir = new Map();
27
+ return {
28
+ resolve(specifier, fromFile) {
29
+ const dir = path.posix.dirname(fromFile);
30
+ let options = optionsByDir.get(dir);
31
+ if (!options) {
32
+ options = compilerOptionsFor(repoPath, dir);
33
+ optionsByDir.set(dir, options);
34
+ }
35
+ const containing = path.posix.join(root, fromFile);
36
+ const result = ts.resolveModuleName(specifier, containing, options, host).resolvedModule;
37
+ if (!result)
38
+ return undefined;
39
+ const found = rel(result.resolvedFileName);
40
+ return found.startsWith('..') || !files.has(found) ? undefined : found;
41
+ },
42
+ };
43
+ }
@@ -0,0 +1,37 @@
1
+ import ts from 'typescript';
2
+ /** True when the file's leading comments hold `@<name>` at the start of a comment (after `//`, `///` or `/*`). */
3
+ export function hasLeadingDirective(sourceFile, name) {
4
+ const text = sourceFile.text;
5
+ const pattern = new RegExp(`^(?:\\/\\/\\/?|\\/\\*+)\\s*@${name}\\b`);
6
+ return (ts.getLeadingCommentRanges(text, 0) ?? []).some((range) => pattern.test(text.slice(range.pos, range.end)));
7
+ }
8
+ const LINE_DIRECTIVE = /^\/\/\/?\s*@(?:ts-expect-error|ts-ignore)/;
9
+ const BLOCK_DIRECTIVE = /^(?:\/|\*)*\s*@(?:ts-expect-error|ts-ignore)/;
10
+ /** Number of `@ts-ignore` and `@ts-expect-error` comments, found through the AST so strings, templates, regexes and JSX text never count. */
11
+ export function countSuppressionComments(sourceFile) {
12
+ const text = sourceFile.text;
13
+ const seen = new Set();
14
+ let count = 0;
15
+ const check = (ranges) => {
16
+ for (const range of ranges ?? []) {
17
+ if (seen.has(range.pos))
18
+ continue;
19
+ seen.add(range.pos);
20
+ const comment = text.slice(range.pos, range.end);
21
+ const matches = range.kind === ts.SyntaxKind.SingleLineCommentTrivia
22
+ ? LINE_DIRECTIVE.test(comment)
23
+ : BLOCK_DIRECTIVE.test(comment.slice(comment.lastIndexOf('\n') + 1));
24
+ if (matches)
25
+ count += 1;
26
+ }
27
+ };
28
+ const visit = (node) => {
29
+ if (node.kind !== ts.SyntaxKind.JsxText) {
30
+ check(ts.getLeadingCommentRanges(text, node.pos));
31
+ check(ts.getTrailingCommentRanges(text, node.end));
32
+ }
33
+ ts.forEachChild(node, visit);
34
+ };
35
+ ts.forEachChild(sourceFile, visit);
36
+ return count;
37
+ }
@@ -0,0 +1,60 @@
1
+ import ts from 'typescript';
2
+ function hasModifier(node, kind) {
3
+ return ts.canHaveModifiers(node) && (ts.getModifiers(node)?.some((m) => m.kind === kind) ?? false);
4
+ }
5
+ function bindingNames(name, out) {
6
+ if (ts.isIdentifier(name)) {
7
+ out.add(name.text);
8
+ return;
9
+ }
10
+ for (const element of name.elements) {
11
+ if (ts.isBindingElement(element))
12
+ bindingNames(element.name, out);
13
+ }
14
+ }
15
+ /**
16
+ * Exported symbols of one file, the denominator of 4.4:
17
+ * each exported declaration by name (overloads and merged declarations once, each binding of a
18
+ * destructured `const` once), each specifier of `export { a, b as c }` and `export { x } from`,
19
+ * `export default` once, `export =` once, `export * as ns from` once, `export * from` zero.
20
+ */
21
+ export function countExportedSymbols(sourceFile) {
22
+ const names = new Set();
23
+ let extra = 0;
24
+ for (const statement of sourceFile.statements) {
25
+ if (ts.isExportAssignment(statement)) {
26
+ extra += 1; // `export default expr` and `export = expr`
27
+ continue;
28
+ }
29
+ if (ts.isExportDeclaration(statement)) {
30
+ const clause = statement.exportClause;
31
+ if (!clause)
32
+ continue; // export * from '...'
33
+ if (ts.isNamespaceExport(clause))
34
+ extra += 1;
35
+ else
36
+ extra += clause.elements.length;
37
+ continue;
38
+ }
39
+ if (!hasModifier(statement, ts.SyntaxKind.ExportKeyword))
40
+ continue;
41
+ if (hasModifier(statement, ts.SyntaxKind.DefaultKeyword)) {
42
+ extra += 1;
43
+ continue;
44
+ }
45
+ if (ts.isVariableStatement(statement)) {
46
+ for (const declaration of statement.declarationList.declarations)
47
+ bindingNames(declaration.name, names);
48
+ }
49
+ else if (ts.isFunctionDeclaration(statement) ||
50
+ ts.isClassDeclaration(statement) ||
51
+ ts.isInterfaceDeclaration(statement) ||
52
+ ts.isTypeAliasDeclaration(statement) ||
53
+ ts.isEnumDeclaration(statement) ||
54
+ ts.isModuleDeclaration(statement)) {
55
+ if (statement.name)
56
+ names.add(statement.name.getText(sourceFile));
57
+ }
58
+ }
59
+ return names.size + extra;
60
+ }
@@ -0,0 +1,179 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import ts from 'typescript';
4
+ import { trackedPaths } from '../../scope/tracked.js';
5
+ const STRICT_FAMILY = [
6
+ 'noImplicitAny',
7
+ 'strictNullChecks',
8
+ 'strictFunctionTypes',
9
+ 'strictBindCallApply',
10
+ 'strictPropertyInitialization',
11
+ 'noImplicitThis',
12
+ 'useUnknownInCatchVariables',
13
+ 'alwaysStrict',
14
+ ];
15
+ const LEVEL_ORDER = ['none', 'partial', 'strict', 'strict_plus'];
16
+ const posix = (p) => p.split(path.sep).join('/');
17
+ const host = {
18
+ ...ts.sys,
19
+ onUnRecoverableConfigFileDiagnostic: () => undefined,
20
+ };
21
+ /** Parse one tsconfig or jsconfig with `extends` resolved by the TypeScript API. JSON only, nothing is executed. */
22
+ function loadTsConfig(file, cache) {
23
+ const key = posix(file);
24
+ if (cache.has(key))
25
+ return cache.get(key);
26
+ let info;
27
+ const parsed = ts.getParsedCommandLineOfConfigFile(file, {}, host);
28
+ if (parsed) {
29
+ const raw = parsed.raw;
30
+ const references = (parsed.projectReferences ?? []).map((ref) => {
31
+ const resolved = ts.resolveProjectReferencePath(ref);
32
+ return posix(resolved);
33
+ });
34
+ const missingExtends = [];
35
+ for (const error of parsed.errors) {
36
+ if (error.code !== 6053)
37
+ continue; // File '{0}' not found.
38
+ const match = /'([^']+)'/.exec(ts.flattenDiagnosticMessageText(error.messageText, '\n'));
39
+ if (match?.[1])
40
+ missingExtends.push(match[1]);
41
+ }
42
+ info = {
43
+ file: key,
44
+ options: parsed.options,
45
+ fileNames: new Set(parsed.fileNames.map(posix)),
46
+ references,
47
+ solution: Array.isArray(raw?.files) && raw.files.length === 0 && references.length > 0,
48
+ missingExtends,
49
+ };
50
+ }
51
+ cache.set(key, info);
52
+ return info;
53
+ }
54
+ /** The level a set of compiler options declares: every flag is its explicit value, else `strict`, else false. */
55
+ export function levelOf(options) {
56
+ const bag = options;
57
+ const strict = bag.strict === true;
58
+ const effective = (flag) => (typeof bag[flag] === 'boolean' ? bag[flag] === true : strict);
59
+ const turnedOff = STRICT_FAMILY.some((flag) => bag[flag] === false);
60
+ if (strict && !turnedOff)
61
+ return options.noUncheckedIndexedAccess === true ? 'strict_plus' : 'strict';
62
+ return STRICT_FAMILY.some(effective) ? 'partial' : 'none';
63
+ }
64
+ export function lowestLevel(levels) {
65
+ return levels.reduce((low, level) => (LEVEL_ORDER.indexOf(level) < LEVEL_ORDER.indexOf(low) ? level : low), 'strict_plus');
66
+ }
67
+ function reachable(root, cache) {
68
+ const seen = new Set([root.file]);
69
+ const out = [];
70
+ const queue = [...root.references];
71
+ while (queue.length > 0) {
72
+ const next = queue.shift();
73
+ if (next === undefined || seen.has(next))
74
+ continue;
75
+ seen.add(next);
76
+ const info = loadTsConfig(next, cache);
77
+ if (!info)
78
+ continue;
79
+ out.push(info);
80
+ queue.push(...info.references);
81
+ }
82
+ return out;
83
+ }
84
+ function covers(config, files) {
85
+ for (const file of config.fileNames)
86
+ if (files.has(file))
87
+ return true;
88
+ return false;
89
+ }
90
+ function configIn(dir) {
91
+ for (const name of ['tsconfig.json', 'jsconfig.json']) {
92
+ const candidate = path.join(dir, name);
93
+ if (existsSync(candidate))
94
+ return candidate;
95
+ }
96
+ return undefined;
97
+ }
98
+ /**
99
+ * The configs that apply to one unit: the package's own `tsconfig.json`, else its `jsconfig.json`,
100
+ * else the nearest ancestor config up to the repository root, kept only if it (or a config it
101
+ * references) covers a logic file of the unit. A solution-style config is replaced by the configs
102
+ * reachable through its `references` that cover a logic file of the unit (the config itself if none do).
103
+ */
104
+ export function unitConfigs(repoPath, packageDir, logicFiles, cache) {
105
+ const files = new Set(logicFiles.map((f) => posix(path.resolve(repoPath, f))));
106
+ const root = path.resolve(repoPath);
107
+ const start = path.resolve(repoPath, packageDir);
108
+ let chosen = configIn(start);
109
+ let ancestor = false;
110
+ if (!chosen) {
111
+ ancestor = true;
112
+ for (let dir = path.dirname(start);; dir = path.dirname(dir)) {
113
+ if (dir.length < root.length)
114
+ break;
115
+ chosen = configIn(dir);
116
+ if (chosen || dir === root)
117
+ break;
118
+ }
119
+ }
120
+ if (!chosen)
121
+ return { configs: [], read: [] };
122
+ const top = loadTsConfig(chosen, cache);
123
+ if (!top)
124
+ return { configs: [], read: [] };
125
+ const nested = reachable(top, cache);
126
+ const read = [top, ...nested];
127
+ if (ancestor && ![top, ...nested].some((c) => covers(c, files)))
128
+ return { configs: [], read: [] };
129
+ if (!top.solution)
130
+ return { configs: [top], read };
131
+ const covering = nested.filter((c) => !c.solution && covers(c, files));
132
+ return { configs: covering.length > 0 ? covering : [top], read };
133
+ }
134
+ /** The nearest `tsconfig.json`, else `jsconfig.json`, from `dir` up to the repository root. Absolute path. */
135
+ export function nearestTsConfig(repoPath, dir) {
136
+ const root = path.resolve(repoPath);
137
+ for (let current = path.resolve(root, dir); current.length >= root.length; current = path.dirname(current)) {
138
+ const found = configIn(current);
139
+ if (found)
140
+ return found;
141
+ if (current === root)
142
+ break;
143
+ }
144
+ return undefined;
145
+ }
146
+ const optionsCache = new Map();
147
+ /**
148
+ * The compiler options of the nearest `tsconfig.json`, else `jsconfig.json`, with `extends` chains resolved through a host that only
149
+ * sees tracked files (so `node_modules` is never read). Without a config: `Bundler` resolution, ESNext modules, `allowJs`, JSON modules.
150
+ */
151
+ export function compilerOptionsFor(repoPath, dir) {
152
+ const fallback = {
153
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
154
+ module: ts.ModuleKind.ESNext,
155
+ allowJs: true,
156
+ resolveJsonModule: true,
157
+ };
158
+ const configFile = nearestTsConfig(repoPath, dir);
159
+ if (!configFile)
160
+ return fallback;
161
+ const cached = optionsCache.get(configFile);
162
+ if (cached)
163
+ return cached;
164
+ const root = path.resolve(repoPath);
165
+ const tracked = new Set(trackedPaths(repoPath));
166
+ const rel = (abs) => path.posix.relative(posix(root), posix(abs));
167
+ const visible = (abs) => tracked.has(rel(abs));
168
+ const configHost = {
169
+ useCaseSensitiveFileNames: true,
170
+ readDirectory: () => [],
171
+ fileExists: visible,
172
+ readFile: (abs) => (visible(abs) ? ts.sys.readFile(abs) : undefined),
173
+ };
174
+ const read = ts.readConfigFile(configFile, (abs) => configHost.readFile(abs));
175
+ const parsed = read.error ? undefined : ts.parseJsonConfigFileContent(read.config, configHost, path.dirname(configFile));
176
+ const options = { ...fallback, ...(parsed?.options ?? {}), allowJs: true };
177
+ optionsCache.set(configFile, options);
178
+ return options;
179
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,41 @@
1
+ export function logicFiles(scope, group) {
2
+ return scope.files.filter((f) => f.role === 'logic' && f.group === group);
3
+ }
4
+ export function unitId(file) {
5
+ return `${file.package}\0${file.group}`;
6
+ }
7
+ /** One tally per (package, group) that has logic files, weight = logic lines of the unit. */
8
+ export function emptyTallies(files) {
9
+ const tallies = new Map();
10
+ for (const file of files) {
11
+ const key = unitId(file);
12
+ const tally = tallies.get(key) ?? { package: file.package, group: file.group, weight: 0, numerator: 0, denominator: 0 };
13
+ tally.weight += file.logicLines;
14
+ tallies.set(key, tally);
15
+ }
16
+ return tallies;
17
+ }
18
+ export function tallyUnits(tallies) {
19
+ return [...tallies.values()]
20
+ .sort((a, b) => a.package.localeCompare(b.package) || a.group.localeCompare(b.group))
21
+ .map((t) => ({
22
+ package: t.package,
23
+ group: t.group,
24
+ weight: t.weight,
25
+ raw: { kind: 'ratio', numerator: t.numerator, denominator: t.denominator, scale: 100 },
26
+ }));
27
+ }
28
+ /** One unit per (workspace package, language group) that has at least one logic-role file, sorted by package then group. */
29
+ export function logicUnits(scope) {
30
+ const units = new Map();
31
+ for (const file of scope.files) {
32
+ if (file.role !== 'logic')
33
+ continue;
34
+ const key = unitId(file);
35
+ const unit = units.get(key) ?? { package: file.package, group: file.group, dir: file.package, weight: 0, files: [] };
36
+ unit.weight += file.logicLines;
37
+ unit.files.push(file);
38
+ units.set(key, unit);
39
+ }
40
+ return [...units.values()].sort((a, b) => a.package.localeCompare(b.package) || a.group.localeCompare(b.group));
41
+ }
@@ -0,0 +1,159 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { parse } from 'yaml';
5
+ import { dimensionFileSchema, methodologyFileSchema, presetFileSchema, scopeFileSchema, } from './schema.js';
6
+ export class MethodologyError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = 'MethodologyError';
10
+ }
11
+ }
12
+ /** Package root: the directory holding `methodology/`, both from `src/` and from `dist/`. */
13
+ /** The methodology version a scan uses when `--methodology` is not given. */
14
+ export const CURRENT_METHODOLOGY_VERSION = '1.0';
15
+ export const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
16
+ function readYaml(file, schema) {
17
+ let doc;
18
+ try {
19
+ doc = parse(readFileSync(file, 'utf8'));
20
+ }
21
+ catch (err) {
22
+ throw new MethodologyError(`${file}: ${err instanceof Error ? err.message : String(err)}`);
23
+ }
24
+ const result = schema.safeParse(doc);
25
+ if (!result.success) {
26
+ const issue = result.error.issues[0];
27
+ const where = issue && issue.path.length > 0 ? issue.path.join('.') : '(root)';
28
+ throw new MethodologyError(`${file}: ${where}: ${issue?.message ?? 'invalid'}`);
29
+ }
30
+ return result.data;
31
+ }
32
+ function commaList(text) {
33
+ return text
34
+ .split(',')
35
+ .map((entry) => entry.trim().replace(/^node:/, ''))
36
+ .filter((entry) => entry !== '');
37
+ }
38
+ function parseD3Lists(def, file = 'dimension 3') {
39
+ const lists = { forbiddenImports: { typescript_javascript: [], python: [] }, infrastructureDirectories: [] };
40
+ if (!def)
41
+ return lists;
42
+ const measure = def.measure ?? {};
43
+ for (const group of ['typescript_javascript', 'python']) {
44
+ const match = /Forbidden imports:\s*(.+?)\.(?:\s|$)/s.exec(measure[group] ?? '');
45
+ if (!match?.[1])
46
+ throw new MethodologyError(`${file}: parameter ${def.id} measure.${group} has no "Forbidden imports:" list`);
47
+ lists.forbiddenImports[group] = [...new Set(commaList(match[1]))];
48
+ }
49
+ const infra = (measure.infrastructure_directories ?? '').replace(/\.\s*$/, '');
50
+ lists.infrastructureDirectories = commaList(infra);
51
+ if (lists.infrastructureDirectories.length === 0) {
52
+ throw new MethodologyError(`${file}: parameter ${def.id} measure.infrastructure_directories is empty`);
53
+ }
54
+ return lists;
55
+ }
56
+ function words(text) {
57
+ return text
58
+ .replace(/\s+/g, ' ')
59
+ .replace(/,?\s+(?:and|or)\s+/g, ', ')
60
+ .split(/[,;]/)
61
+ .map((entry) => entry.trim())
62
+ .filter((entry) => entry !== '');
63
+ }
64
+ function parseD6Lists(scriptResolution, defs, defs6x, file = 'dimension 6') {
65
+ const text = (scriptResolution ?? '').replace(/\s+/g, ' ');
66
+ const pick = (re, what) => {
67
+ const match = re.exec(text);
68
+ if (!match?.[1])
69
+ throw new MethodologyError(`${file}: script_resolution has no ${what} list`);
70
+ return words(match[1]);
71
+ };
72
+ const typecheckers = pick(/typecheckers (.+?);/, 'typecheckers');
73
+ const linters = pick(/linters (.+?);/, 'linters');
74
+ const testRunners = pick(/test runners (.+?)\. The prefixes/, 'test runners');
75
+ const prefixes = pick(/The prefixes (.+?) are removed/, 'prefixes');
76
+ const env = defs?.scoring.method === 'mean_of_components' ? defs.scoring.components.find((c) => c.key === 'env_documented') : undefined;
77
+ const runtime = defs?.scoring.method === 'mean_of_components' ? defs.scoring.components.find((c) => c.key === 'runtime_pinned') : undefined;
78
+ const envMeasure = (env?.measure ?? '').replace(/\s+/g, ' ');
79
+ const examples = /Example files: (.+?)\. Reads:/.exec(envMeasure)?.[1];
80
+ const excluded = /Excluded names: (.+?), and names starting with (.+?)\.?$/.exec(envMeasure);
81
+ if (!examples || !excluded?.[1] || !excluded[2])
82
+ throw new MethodologyError(`${file}: parameter 6.5 env_documented measure has no example files or excluded names`);
83
+ const pins = /^Any of (.+?)\.?$/.exec((runtime?.measure ?? '').replace(/\s+/g, ' '))?.[1];
84
+ if (!pins)
85
+ throw new MethodologyError(`${file}: parameter 6.5 runtime_pinned measure has no file list`);
86
+ const docMeasure = (defs6x?.measure?.all_languages ?? '').replace(/\s+/g, ' ');
87
+ const managers = /starts with a package manager \((.+?)\)/.exec(docMeasure)?.[1];
88
+ if (!managers)
89
+ throw new MethodologyError(`${file}: parameter 6.6 measure has no package manager list`);
90
+ return {
91
+ typecheckers,
92
+ linters,
93
+ testRunners,
94
+ prefixes,
95
+ envExcludedNames: words(excluded[1]),
96
+ envExcludedPrefixes: words(excluded[2]),
97
+ envExampleFiles: words(examples),
98
+ runtimePinFiles: words(pins),
99
+ docPackageManagers: words(managers),
100
+ };
101
+ }
102
+ export function loadMethodology(version, rootDir = packageRoot) {
103
+ if (!/^\d+\.\d+$/.test(version)) {
104
+ throw new MethodologyError(`Unknown methodology version "${version}"`);
105
+ }
106
+ const dir = path.join(rootDir, 'methodology', `v${version}`);
107
+ if (!existsSync(dir)) {
108
+ throw new MethodologyError(`Unknown methodology version "${version}" (no ${dir})`);
109
+ }
110
+ const main = readYaml(path.join(dir, 'methodology.yaml'), methodologyFileSchema);
111
+ if (main.version !== version) {
112
+ throw new MethodologyError(`${path.join(dir, 'methodology.yaml')}: version "${main.version}" does not match directory v${version}`);
113
+ }
114
+ const scope = readYaml(path.join(dir, 'scope.yaml'), scopeFileSchema);
115
+ const presets = {};
116
+ const dimensions = main.dimensions.map((entry) => {
117
+ const file = path.join(dir, entry.file);
118
+ const parsed = readYaml(file, dimensionFileSchema);
119
+ if (parsed.dimension.id !== entry.id || parsed.dimension.key !== entry.key) {
120
+ throw new MethodologyError(`${file}: dimension id/key do not match methodology.yaml`);
121
+ }
122
+ const total = parsed.parameters.reduce((sum, p) => sum + p.weight, 0);
123
+ if (Math.abs(total - 100) > 1e-9) {
124
+ throw new MethodologyError(`${file}: parameter weights sum to ${total}, expected 100`);
125
+ }
126
+ for (const presetFile of parsed.presets?.files ?? []) {
127
+ const loaded = readYaml(path.join(dir, presetFile), presetFileSchema);
128
+ presets[loaded.preset.key] = {
129
+ preset: loaded.preset.key,
130
+ name: loaded.preset.name,
131
+ ...(loaded.conventions === undefined ? {} : { conventions: loaded.conventions }),
132
+ languages: loaded.preset.languages,
133
+ layers: loaded.layers,
134
+ modules: loaded.modules,
135
+ rules: loaded.rules,
136
+ };
137
+ }
138
+ return {
139
+ id: parsed.dimension.id,
140
+ key: parsed.dimension.key,
141
+ name: parsed.dimension.name,
142
+ ...(parsed.script_resolution === undefined ? {} : { scriptResolution: parsed.script_resolution }),
143
+ parameters: parsed.parameters,
144
+ };
145
+ });
146
+ const d3Def = dimensions.find((d) => d.id === 3)?.parameters.find((p) => p.id === '3.5');
147
+ const d6Dim = dimensions.find((d) => d.id === 6);
148
+ return {
149
+ version,
150
+ presets,
151
+ d3: parseD3Lists(d3Def, path.join(dir, 'dimensions')),
152
+ d6: parseD6Lists(d6Dim?.scriptResolution, d6Dim?.parameters.find((p) => p.id === '6.5'), d6Dim?.parameters.find((p) => p.id === '6.6'), path.join(dir, 'dimensions')),
153
+ dimensions,
154
+ capMargin: main.aggregation.dimension_to_health.cap_margin,
155
+ bands: main.bands,
156
+ scope,
157
+ execution: main.execution,
158
+ };
159
+ }