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,371 @@
1
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import ts from 'typescript';
5
+ import { classifyImports, hasUseClient } from './classify.js';
6
+ import { findDeclaredConfig } from './config.js';
7
+ import { isDomainFile, isImpure } from './domain.js';
8
+ import { presetRules } from './rules.js';
9
+ import { readPackageJson } from '../../../manifest/package-json.js';
10
+ import { trackedPaths } from '../../../scope/tracked.js';
11
+ import { ciInvokes } from '../../../scripts/expand.js';
12
+ import { nearestTsConfig } from '../../ts/tsconfig.js';
13
+ import { choosePreset, globLayers, globMatcher, moduleDirs, publicEntryGlob } from '../presets.js';
14
+ import { edgeKey } from '../types.js';
15
+ const GROUP = 'typescript_javascript';
16
+ const TOOL = 'dependency-cruiser';
17
+ function setup(ctx) {
18
+ return ctx.tools.memo(`d3:ts:setup:${ctx.repoPath}`, async () => {
19
+ const warnings = [];
20
+ const tracked = new Set(trackedPaths(ctx.repoPath));
21
+ const byPackage = new Map();
22
+ for (const file of ctx.scope.files) {
23
+ if (file.group !== GROUP)
24
+ continue;
25
+ const list = byPackage.get(file.package) ?? [];
26
+ list.push(file);
27
+ byPackage.set(file.package, list);
28
+ }
29
+ const ciHits = ciInvokes(ctx.repoPath, ['depcruise', 'dependency-cruiser'], { warnings });
30
+ const configCache = new Map();
31
+ const units = [];
32
+ for (const [pkg, files] of [...byPackage].sort(([a], [b]) => a.localeCompare(b))) {
33
+ const logic = files.filter((f) => f.role === 'logic');
34
+ if (logic.length === 0)
35
+ continue;
36
+ const manifestPath = path.posix.join(pkg, 'package.json');
37
+ const manifest = readPackageJson(ctx.repoPath, pkg);
38
+ if (manifest === undefined && tracked.has(manifestPath))
39
+ warnings.push(`${manifestPath}: invalid JSON, treated as absent`);
40
+ const deps = manifest?.dependencies ?? {};
41
+ const preset = choosePreset(ctx.methodology, GROUP, { nextjs: 'next' in deps, express: 'express' in deps && !('next' in deps) });
42
+ const declared = findDeclaredConfig(ctx.repoPath, pkg, warnings, configCache);
43
+ const weight = logic.reduce((sum, f) => sum + f.logicLines, 0);
44
+ const level31 = declared === undefined ? 'none' : ciHits.length > 0 ? 'enforced' : 'declared';
45
+ units.push({
46
+ package: pkg,
47
+ files,
48
+ logic,
49
+ weight,
50
+ preset,
51
+ declared,
52
+ rules: {
53
+ package: pkg,
54
+ group: GROUP,
55
+ weight,
56
+ preset: preset.preset,
57
+ rulesSource: declared === undefined ? 'preset' : 'declared',
58
+ ...(declared === undefined ? {} : { rulesFile: declared.file }),
59
+ level31,
60
+ },
61
+ });
62
+ }
63
+ return { units, warnings };
64
+ });
65
+ }
66
+ /** The installed version, read from the package's own manifest (its `exports` map hides `package.json`). */
67
+ function dcVersion() {
68
+ for (let dir = path.dirname(fileURLToPath(import.meta.url)); dir !== path.dirname(dir); dir = path.dirname(dir)) {
69
+ const manifest = path.join(dir, 'node_modules', TOOL, 'package.json');
70
+ if (!existsSync(manifest))
71
+ continue;
72
+ try {
73
+ return JSON.parse(readFileSync(manifest, 'utf8')).version;
74
+ }
75
+ catch {
76
+ return undefined;
77
+ }
78
+ }
79
+ return undefined;
80
+ }
81
+ const configHost = { ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => undefined };
82
+ async function runCruise(cruise, input, extensions, warnings) {
83
+ const attempt = async (tsConfig) => {
84
+ // `paths` aliases need the parsed config as the transpile options; without it the resolver reads `baseUrl` from the process cwd.
85
+ const resolveOptions = {
86
+ extensionAlias: { '.js': ['.ts', '.tsx', '.js'], '.mjs': ['.mts', '.mjs'], '.cjs': ['.cts', '.cjs'] },
87
+ extensions,
88
+ };
89
+ const parsed = tsConfig === undefined ? undefined : ts.getParsedCommandLineOfConfigFile(tsConfig, {}, configHost);
90
+ if (tsConfig !== undefined && parsed === undefined)
91
+ throw new Error('the config could not be parsed');
92
+ const hasRules = input.ruleSet !== undefined && ((input.ruleSet.forbidden?.length ?? 0) > 0 || (input.ruleSet.allowed?.length ?? 0) > 0);
93
+ const options = {
94
+ baseDir: input.baseDir,
95
+ validate: hasRules,
96
+ cache: false,
97
+ tsPreCompilationDeps: true,
98
+ doNotFollow: { path: 'node_modules' },
99
+ ...(hasRules ? { ruleSet: input.ruleSet } : {}),
100
+ ...(tsConfig === undefined ? {} : { tsConfig: { fileName: tsConfig } }),
101
+ };
102
+ // dependency-cruiser gives tsconfig-paths-webpack-plugin `baseUrl: "./"` when the config has no baseUrl, and the plugin resolves
103
+ // that against the process cwd, so `paths` without `baseUrl` (the Next.js default) pointed at the wrong directory and every alias
104
+ // import was unresolved. With a baseUrl in the transpile options it passes none and the plugin uses the config's own directory.
105
+ const transpile = parsed !== undefined && parsed.options.paths !== undefined && parsed.options.baseUrl === undefined
106
+ ? { ...parsed, options: { ...parsed.options, baseUrl: String(parsed.options.pathsBasePath ?? path.dirname(tsConfig)) } }
107
+ : parsed;
108
+ const result = await cruise(input.files, options, resolveOptions, transpile === undefined ? undefined : { tsConfig: transpile });
109
+ const output = result.output;
110
+ return { modules: output.modules ?? [], violations: output.summary?.violations ?? [] };
111
+ };
112
+ if (input.tsConfig === undefined)
113
+ return attempt(undefined);
114
+ try {
115
+ return await attempt(input.tsConfig);
116
+ }
117
+ catch (err) {
118
+ warnings.push(`${path.relative(input.baseDir, input.tsConfig)}: could not be used by ${TOOL}, aliases are unresolved (${err instanceof Error ? err.message : String(err)})`);
119
+ return attempt(undefined);
120
+ }
121
+ }
122
+ function isTypeOnlyEdge(occurrences) {
123
+ return occurrences.length > 0 && occurrences.every((o) => o.typeOnly);
124
+ }
125
+ function isRuntimeEdge(occurrences) {
126
+ if (occurrences.length === 0)
127
+ return true;
128
+ return occurrences.some((o) => (o.kind === 'static' || o.kind === 'require') && o.topLevel && !o.typeOnly);
129
+ }
130
+ export const tsAdapter = {
131
+ group: GROUP,
132
+ async rules(ctx) {
133
+ const { units, warnings } = await setup(ctx);
134
+ return { units: units.map((u) => u.rules), warnings };
135
+ },
136
+ async graph(ctx) {
137
+ return ctx.tools.memo(`d3:ts:graph:${ctx.repoPath}`, () => buildGraph(ctx));
138
+ },
139
+ };
140
+ async function buildGraph(ctx) {
141
+ const { units } = await setup(ctx);
142
+ const warnings = [];
143
+ let cruise;
144
+ try {
145
+ cruise = (await import(TOOL)).cruise;
146
+ }
147
+ catch (err) {
148
+ return { unavailable: `${TOOL} could not be loaded: ${err instanceof Error ? err.message : String(err)}` };
149
+ }
150
+ const version = dcVersion();
151
+ if (version !== undefined)
152
+ ctx.tools.recordVersion(TOOL, version);
153
+ // dependency-cruiser follows symlinks, so its resolved paths are relative to the real repo root (macOS tmpdir sits behind /var -> /private/var).
154
+ const root = realpathSync(ctx.repoPath);
155
+ const extensions = [...ctx.methodology.scope.source_files.extensions.typescript, ...ctx.methodology.scope.source_files.extensions.javascript];
156
+ const scoped = new Map();
157
+ for (const file of ctx.scope.files)
158
+ if (file.group === GROUP)
159
+ scoped.set(file.path, file);
160
+ const unitByPackage = new Map(units.map((u) => [u.package, u]));
161
+ const relative = (unit, file) => (unit.package === '.' ? file : file.slice(unit.package.length + 1));
162
+ // Layer classification and the preset rules of preset-governed units.
163
+ const dcRules = [];
164
+ const meta = new Map();
165
+ for (const unit of units) {
166
+ if (unit.declared !== undefined)
167
+ continue;
168
+ const rels = unit.files.map((f) => relative(unit, f.path));
169
+ const toFull = new Map(rels.map((rel, i) => [rel, unit.files[i].path]));
170
+ const layerFiles = new Map();
171
+ const add = (layer, rel) => {
172
+ const set = layerFiles.get(layer) ?? new Set();
173
+ set.add(toFull.get(rel));
174
+ layerFiles.set(layer, set);
175
+ };
176
+ for (const [rel, layer] of globLayers(unit.preset.layers, rels))
177
+ add(layer, rel);
178
+ const contentLayers = Object.entries(unit.preset.layers).filter((entry) => typeof entry[1] === 'string').map(([name]) => name);
179
+ if (contentLayers.includes('client') || contentLayers.includes('server_only')) {
180
+ const isServerDir = globMatcher(['server/**']);
181
+ for (const rel of rels) {
182
+ const full = toFull.get(rel);
183
+ const source = ctx.scope.getTsSourceFile(full);
184
+ if (contentLayers.includes('client') && hasUseClient(source))
185
+ add('client', rel);
186
+ if (contentLayers.includes('server_only')) {
187
+ const imports = classifyImports(source).some((o) => o.specifier === 'server-only');
188
+ if (imports || path.posix.basename(rel).includes('.server.') || isServerDir(rel))
189
+ add('server_only', rel);
190
+ }
191
+ }
192
+ }
193
+ const generated = presetRules(unit.package, unit.preset.rules, layerFiles, unit.files.map((f) => f.path));
194
+ dcRules.push(...generated.rules);
195
+ for (const [name, value] of generated.meta)
196
+ meta.set(name, value);
197
+ }
198
+ // One graph call per distinct tsconfig; every call carries all generated preset rules.
199
+ const groups = new Map();
200
+ for (const unit of units) {
201
+ const key = nearestTsConfig(root, unit.package === '.' ? '' : unit.package) ?? '';
202
+ const list = groups.get(key) ?? [];
203
+ list.push(...unit.files.map((f) => f.path));
204
+ groups.set(key, list);
205
+ }
206
+ const allEdges = new Map();
207
+ const violations = [];
208
+ try {
209
+ for (const [tsConfig, files] of groups) {
210
+ const result = await runCruise(cruise, { baseDir: root, files, tsConfig: tsConfig === '' ? undefined : tsConfig, ruleSet: { forbidden: dcRules } }, extensions, warnings);
211
+ for (const module of result.modules) {
212
+ if (!scoped.has(module.source))
213
+ continue;
214
+ const perTarget = allEdges.get(module.source) ?? new Map();
215
+ for (const dependency of module.dependencies) {
216
+ if (dependency.resolved === module.source)
217
+ continue;
218
+ const specifiers = perTarget.get(dependency.resolved) ?? new Set();
219
+ specifiers.add(dependency.module);
220
+ perTarget.set(dependency.resolved, specifiers);
221
+ }
222
+ allEdges.set(module.source, perTarget);
223
+ }
224
+ violations.push(...result.violations);
225
+ }
226
+ // One extra call per distinct declared rules file; it supplies only violations.
227
+ const declaredFiles = new Map();
228
+ for (const unit of units) {
229
+ if (unit.declared === undefined)
230
+ continue;
231
+ const entry = declaredFiles.get(unit.declared.file) ?? { config: unit.declared, governed: new Set() };
232
+ entry.governed.add(unit.package);
233
+ declaredFiles.set(unit.declared.file, entry);
234
+ }
235
+ for (const { config, governed } of declaredFiles.values()) {
236
+ const dir = path.posix.dirname(config.file);
237
+ const prefix = dir === '.' ? '' : `${dir}/`;
238
+ const files = [...scoped.keys()].filter((f) => f.startsWith(prefix)).map((f) => f.slice(prefix.length));
239
+ if (files.length === 0)
240
+ continue;
241
+ const baseDir = dir === '.' ? root : path.join(root, dir);
242
+ const ruleSet = {
243
+ forbidden: config.forbidden,
244
+ ...(config.allowed ? { allowed: config.allowed, allowedSeverity: config.allowedSeverity } : {}),
245
+ };
246
+ const result = await runCruise(cruise, { baseDir, files, tsConfig: nearestTsConfig(root, dir), ruleSet }, extensions, warnings);
247
+ for (const violation of result.violations) {
248
+ const from = path.posix.join(dir, violation.from);
249
+ const unit = unitByPackage.get(scoped.get(from)?.package ?? '');
250
+ if (!unit || !governed.has(unit.package))
251
+ continue;
252
+ const joined = path.posix.join(dir, violation.to);
253
+ const local = (violation.dependencyTypes ?? []).includes('local');
254
+ if (scoped.has(joined))
255
+ violations.push({ ...violation, from, to: joined });
256
+ else if (!local)
257
+ violations.push({ ...violation, from });
258
+ }
259
+ }
260
+ }
261
+ catch (err) {
262
+ return { unavailable: `${TOOL} failed: ${err instanceof Error ? err.message : String(err)}` };
263
+ }
264
+ // Facts per unit.
265
+ const occurrencesOf = new Map();
266
+ const occurrences = (file) => {
267
+ let list = occurrencesOf.get(file);
268
+ if (!list) {
269
+ list = classifyImports(ctx.scope.getTsSourceFile(file));
270
+ occurrencesOf.set(file, list);
271
+ }
272
+ return list;
273
+ };
274
+ const edgeOccurrences = (from, to) => {
275
+ const specifiers = allEdges.get(from)?.get(to);
276
+ return specifiers ? occurrences(from).filter((o) => specifiers.has(o.specifier)) : [];
277
+ };
278
+ const moduleOf = new Map();
279
+ const entryOf = new Set();
280
+ const trackedSet = new Set(trackedPaths(ctx.repoPath));
281
+ const entryChecks = new Map();
282
+ for (const unit of units) {
283
+ const roots = Array.isArray(unit.preset.modules.roots) ? unit.preset.modules.roots : [];
284
+ const entryGlob = publicEntryGlob(unit.preset, GROUP);
285
+ const isEntry = entryGlob === undefined ? () => false : globMatcher([entryGlob.includes('/') ? entryGlob : `**/${entryGlob}`]);
286
+ entryChecks.set(unit.package, { roots, isEntry });
287
+ const rels = unit.files.map((f) => relative(unit, f.path));
288
+ const modules = moduleDirs(roots, rels);
289
+ unit.files.forEach((file, i) => {
290
+ const rel = rels[i];
291
+ const dir = modules.get(rel);
292
+ const full = dir === undefined ? undefined : unit.package === '.' ? dir : `${unit.package}/${dir}`;
293
+ moduleOf.set(file.path, full);
294
+ if (dir !== undefined && path.posix.dirname(rel) === dir && isEntry(rel))
295
+ entryOf.add(file.path);
296
+ });
297
+ }
298
+ const result = [];
299
+ for (const unit of units) {
300
+ const unitFiles = new Set(unit.files.map((f) => f.path));
301
+ const edges = [];
302
+ for (const file of unit.files) {
303
+ for (const to of [...(allEdges.get(file.path)?.keys() ?? [])].sort()) {
304
+ if (!scoped.has(to))
305
+ continue;
306
+ const found = edgeOccurrences(file.path, to);
307
+ edges.push({ from: file.path, to, fromTest: file.role === 'test', runtime: isRuntimeEdge(found) });
308
+ }
309
+ }
310
+ const violating = new Set();
311
+ const violationRules = {};
312
+ for (const violation of violations) {
313
+ if (violation.type !== 'dependency' || (violation.rule.severity !== 'error' && violation.rule.severity !== 'warn'))
314
+ continue;
315
+ if (!unitFiles.has(violation.from) || scoped.get(violation.from)?.role === 'test')
316
+ continue;
317
+ const info = meta.get(violation.rule.name);
318
+ if (info?.excludesTypeOnly && isTypeOnlyEdge(edgeOccurrences(violation.from, violation.to)))
319
+ continue;
320
+ const key = edgeKey(violation.from, violation.to);
321
+ violating.add(key);
322
+ (violationRules[key] ??= []).push(info?.rule ?? violation.rule.name);
323
+ }
324
+ const logicFiles = unit.logic.map((f) => f.path).sort();
325
+ const moduleDirsOfUnit = new Set(logicFiles.map((f) => moduleOf.get(f)).filter((m) => m !== undefined));
326
+ const crossings = [];
327
+ if (moduleDirsOfUnit.size >= 2) {
328
+ const prefix = unit.package === '.' ? '' : `${unit.package}/`;
329
+ const check = entryChecks.get(unit.package);
330
+ for (const file of unit.files) {
331
+ if (file.role === 'test')
332
+ continue;
333
+ const fromModule = moduleOf.get(file.path);
334
+ if (fromModule === undefined)
335
+ continue;
336
+ for (const to of [...(allEdges.get(file.path)?.keys() ?? [])].sort()) {
337
+ let toModule;
338
+ let toEntry;
339
+ if (scoped.has(to)) {
340
+ toModule = moduleOf.get(to);
341
+ toEntry = entryOf.has(to);
342
+ }
343
+ else if (trackedSet.has(to) && to.startsWith(prefix) && !to.includes('node_modules/')) {
344
+ // A barrel is not a scoped file, but it is the canonical public entry: count the crossing.
345
+ const rel = to.slice(prefix.length);
346
+ const dir = moduleDirs(check.roots, [rel]).get(rel);
347
+ toModule = dir === undefined ? undefined : `${prefix}${dir}`;
348
+ toEntry = dir !== undefined && path.posix.dirname(rel) === dir && check.isEntry(rel);
349
+ }
350
+ else
351
+ continue;
352
+ if (toModule !== undefined && toModule !== fromModule)
353
+ crossings.push({ from: file.path, to, toEntry });
354
+ }
355
+ }
356
+ }
357
+ const domainFiles = logicFiles.filter(isDomainFile);
358
+ const impure = domainFiles.filter((file) => isImpure(ctx.scope.getTsSourceFile(file), edges.filter((e) => e.from === file).map((e) => e.to), ctx.methodology.d3));
359
+ result.push({
360
+ ...unit.rules,
361
+ logicFiles,
362
+ edges,
363
+ violating: [...violating].sort(),
364
+ violationRules,
365
+ moduleCount: moduleDirsOfUnit.size,
366
+ crossings,
367
+ domain: { files: domainFiles, impure },
368
+ });
369
+ }
370
+ return { units: result, warnings };
371
+ }
@@ -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
+ }