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,65 @@
1
+ import { scoreRaw } from './score.js';
2
+ /**
3
+ * Combine the units of one parameter into one raw value and score.
4
+ * ratio: sum numerators and denominators, score once. Units with a failed raw (their install root's install
5
+ * failed) take no part in the sums; the score is multiplied by the share of the logic lines that is not failed,
6
+ * so a broken package costs its share and cannot be hidden.
7
+ * unit_weighted: score each unit, combine by weight. A failed unit scores 0.
8
+ */
9
+ export function poolParameter(def, units) {
10
+ const scored = units.map((unit) => ({
11
+ ...unit,
12
+ score: scoreRaw(def, unit.raw, unit.group),
13
+ }));
14
+ if (def.pooling === 'ratio') {
15
+ const live = units.filter((unit) => unit.raw.kind !== 'failed');
16
+ const pooled = poolRatio(def, live, scored);
17
+ if (live.length === units.length)
18
+ return pooled;
19
+ return { ...pooled, score: pooled.score * liveShare(live, units) };
20
+ }
21
+ const totalWeight = scored.reduce((sum, unit) => sum + unit.weight, 0);
22
+ if (totalWeight === 0)
23
+ return { raw: null, score: def.raw.when_empty, units: scored };
24
+ const score = scored.reduce((sum, unit) => sum + unit.score * unit.weight, 0) / totalWeight;
25
+ return { raw: null, score, units: scored };
26
+ }
27
+ /** Share of the logic lines (of the units, when none has a line) that belongs to `live`. */
28
+ function liveShare(live, all) {
29
+ const total = all.reduce((sum, unit) => sum + unit.weight, 0);
30
+ if (total === 0)
31
+ return live.length / all.length;
32
+ return live.reduce((sum, unit) => sum + unit.weight, 0) / total;
33
+ }
34
+ function poolRatio(def, units, scored) {
35
+ let numerator = 0;
36
+ let denominator = 0;
37
+ let scale = null;
38
+ let summed = null;
39
+ for (const unit of units) {
40
+ // number raws (5.1 severity points) are summed, their denominator is ignored.
41
+ if (unit.raw.kind === 'number') {
42
+ summed = (summed ?? 0) + unit.raw.value;
43
+ continue;
44
+ }
45
+ if (unit.raw.kind !== 'ratio')
46
+ throw new Error(`${def.id}: ratio pooling needs ratio raws`);
47
+ if (scale !== null && scale !== unit.raw.scale) {
48
+ throw new Error(`${def.id}: units disagree on ratio scale`);
49
+ }
50
+ scale = unit.raw.scale;
51
+ numerator += unit.raw.numerator;
52
+ denominator += unit.raw.denominator;
53
+ }
54
+ if (summed !== null) {
55
+ if (scale !== null)
56
+ throw new Error(`${def.id}: ratio pooling cannot mix number and ratio raws`);
57
+ const raw = { kind: 'number', value: summed };
58
+ return { raw, score: scoreRaw(def, raw), units: scored };
59
+ }
60
+ if (scale === null || denominator === 0) {
61
+ return { raw: null, score: def.raw.when_empty, units: scored };
62
+ }
63
+ const raw = { kind: 'ratio', numerator, denominator, scale };
64
+ return { raw, score: scoreRaw(def, raw), units: scored };
65
+ }
@@ -0,0 +1,81 @@
1
+ function clamp(value, lo, hi) {
2
+ return Math.min(hi, Math.max(lo, value));
3
+ }
4
+ /** The numeric value of a ratio or number raw. A ratio with no denominator has no value. */
5
+ function rawValue(raw) {
6
+ if (raw.kind === 'number')
7
+ return raw.value;
8
+ if (raw.kind === 'ratio') {
9
+ return raw.denominator === 0 ? null : (raw.numerator / raw.denominator) * raw.scale;
10
+ }
11
+ throw new Error(`Raw of kind ${raw.kind} has no numeric value`);
12
+ }
13
+ function piecewiseLinear(anchors, value) {
14
+ const sorted = [...anchors].sort((a, b) => a.raw - b.raw);
15
+ const first = sorted[0];
16
+ const last = sorted[sorted.length - 1];
17
+ if (!first || !last)
18
+ throw new Error('piecewise_linear needs anchors');
19
+ if (value <= first.raw)
20
+ return first.score;
21
+ if (value >= last.raw)
22
+ return last.score;
23
+ for (let i = 1; i < sorted.length; i += 1) {
24
+ const lo = sorted[i - 1];
25
+ const hi = sorted[i];
26
+ if (lo && hi && value <= hi.raw) {
27
+ return lo.score + ((value - lo.raw) / (hi.raw - lo.raw)) * (hi.score - lo.score);
28
+ }
29
+ }
30
+ return last.score;
31
+ }
32
+ function levelScore(levels, level) {
33
+ const found = levels.find((entry) => entry.level === level);
34
+ if (!found)
35
+ throw new Error(`Unknown level "${level}"`);
36
+ return found.score;
37
+ }
38
+ /** Score one raw value. A ratio with no denominator scores `raw.when_empty`. */
39
+ export function scoreRaw(def, raw, group) {
40
+ if (raw.kind === 'failed')
41
+ return 0;
42
+ const scoring = def.scoring;
43
+ switch (scoring.method) {
44
+ case 'piecewise_linear': {
45
+ const value = rawValue(raw);
46
+ return value === null ? def.raw.when_empty : piecewiseLinear(scoring.anchors, value);
47
+ }
48
+ case 'levels': {
49
+ if (raw.kind !== 'level')
50
+ throw new Error(`${def.id}: levels scoring needs a level raw`);
51
+ if (Array.isArray(scoring.levels))
52
+ return levelScore(scoring.levels, raw.level);
53
+ if (!group)
54
+ throw new Error(`${def.id}: a language group is needed to score this level`);
55
+ const forGroup = scoring.levels[group];
56
+ if (!forGroup)
57
+ throw new Error(`${def.id}: no levels for group ${group}`);
58
+ return levelScore(forGroup, raw.level);
59
+ }
60
+ case 'mean_of_components': {
61
+ if (raw.kind !== 'components')
62
+ throw new Error(`${def.id}: needs a components raw`);
63
+ const scores = scoring.components.map((component) => {
64
+ const part = raw.components[component.key];
65
+ if (!part)
66
+ throw new Error(`${def.id}: missing component ${component.key}`);
67
+ if (component.method === 'levels') {
68
+ if (part.kind !== 'level' || !component.levels) {
69
+ throw new Error(`${def.id}.${component.key}: needs a level raw`);
70
+ }
71
+ return levelScore(component.levels, part.level);
72
+ }
73
+ const value = rawValue(part);
74
+ if (value === null)
75
+ return part.kind === 'ratio' ? (part.emptyScore ?? def.raw.when_empty) : def.raw.when_empty;
76
+ return clamp(value, 0, 100);
77
+ });
78
+ return scores.reduce((sum, s) => sum + s, 0) / scores.length;
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,38 @@
1
+ import { dimensionScore, healthScore } from './aggregate.js';
2
+ function weighted(def, param, overrides) {
3
+ const override = overrides.get(def.id);
4
+ if (override !== undefined)
5
+ return { weight: def.weight, score: override };
6
+ if (param?.status === 'failed')
7
+ return { weight: def.weight, score: 0 };
8
+ if (param?.status === 'measured')
9
+ return { weight: def.weight, score: param.score ?? 0 };
10
+ return { weight: def.weight, score: null };
11
+ }
12
+ /** The unrounded health and dimension scores if the overridden parameters had the given scores. Reads the stored parameters only. */
13
+ export function healthWith(run, methodology, overrides) {
14
+ const dimension = new Map();
15
+ for (const dim of methodology.dimensions) {
16
+ dimension.set(dim.id, dimensionScore(dim.parameters.map((def) => weighted(def, run.parameters.find((p) => p.id === def.id), overrides))));
17
+ }
18
+ return { health: healthScore([...dimension.values()], methodology.capMargin), dimension };
19
+ }
20
+ /** The lowest-scoring anchor or level above `score` that has a descriptor. Null when there is none or the parameter has no descriptors. */
21
+ export function nextCandidate(def, score) {
22
+ const scoring = def.scoring;
23
+ let candidates;
24
+ if (scoring.method === 'piecewise_linear')
25
+ candidates = scoring.anchors;
26
+ else if (scoring.method === 'levels')
27
+ candidates = Array.isArray(scoring.levels) ? scoring.levels : Object.values(scoring.levels).flat();
28
+ else
29
+ return null;
30
+ let best;
31
+ for (const candidate of candidates) {
32
+ if (!candidate.descriptor || candidate.score <= score + 1e-9)
33
+ continue;
34
+ if (!best || candidate.score < best.score)
35
+ best = { score: candidate.score, descriptor: candidate.descriptor };
36
+ }
37
+ return best ? { score: best.score, descriptor: best.descriptor.replace(/\s+/g, ' ').trim() } : null;
38
+ }
@@ -0,0 +1,53 @@
1
+ const INFO_FLAGS = new Set(['--help', '--version', '-h', '-V']);
2
+ const TSC_NON_CHECKS = new Set(['--init', '--help', '--version', '-v', '-h']);
3
+ const BIOME_CHECKS = new Set(['check', 'lint', 'ci']);
4
+ /** Tools that run checks the methodology does not recognise (a finding on 6.2). */
5
+ export const UNRECOGNISED_RUNNERS = new Set(['turbo', 'nx', 'lerna', 'next', 'ng', 'react-scripts']);
6
+ /** `python -m unittest` is the tool `unittest` once its prefix is removed; `node --test` is the tool `node` with that flag. */
7
+ function testToolNames(rec) {
8
+ const tools = new Set();
9
+ let nodeTest = false;
10
+ for (const entry of rec.testRunners) {
11
+ if (entry === 'node --test')
12
+ nodeTest = true;
13
+ else
14
+ tools.add(entry.replace(/^python3? -m /, ''));
15
+ }
16
+ return { tools, nodeTest };
17
+ }
18
+ /**
19
+ * The checks a command runs, by tool, after prefix removal. A check is recognised by tool, not by language group, so `tsc` counts
20
+ * for a Python unit in a mixed package and `mypy` for a TypeScript one.
21
+ */
22
+ export function checkKinds(cmd, rec) {
23
+ const out = new Set();
24
+ const tool = cmd.tool;
25
+ if (tool === null)
26
+ return out;
27
+ if (cmd.argv.some((a) => INFO_FLAGS.has(a)))
28
+ return out;
29
+ const first = cmd.argv.find((a) => !a.startsWith('-'));
30
+ if (rec.typecheckers.includes(tool)) {
31
+ if (tool === 'tsc' && cmd.argv.some((a) => TSC_NON_CHECKS.has(a)))
32
+ return out;
33
+ out.add('typecheck');
34
+ return out;
35
+ }
36
+ if (rec.linters.includes(tool)) {
37
+ if (tool === 'ruff' && first === 'format')
38
+ return out;
39
+ if (tool === 'biome' && !(first !== undefined && BIOME_CHECKS.has(first)))
40
+ return out;
41
+ out.add('lint');
42
+ return out;
43
+ }
44
+ const { tools, nodeTest } = testToolNames(rec);
45
+ if (tool === 'tox' || tool === 'nox') {
46
+ if (tools.has(tool) && cmd.readable !== true)
47
+ out.add('test');
48
+ return out;
49
+ }
50
+ if (tools.has(tool) || (nodeTest && tool === 'node' && cmd.argv.includes('--test')))
51
+ out.add('test');
52
+ return out;
53
+ }
@@ -0,0 +1,139 @@
1
+ import path from 'node:path';
2
+ import { parse } from 'yaml';
3
+ import { readTracked, trackedPaths } from '../scope/tracked.js';
4
+ import { triggersFor } from './triggers.js';
5
+ /** The five CI files of methodology 3.1 `ci_files`. */
6
+ function ciFormatOf(file) {
7
+ if (/^\.github\/workflows\/[^/]+\.ya?ml$/.test(file))
8
+ return 'github';
9
+ if (file === '.gitlab-ci.yml')
10
+ return 'gitlab';
11
+ if (file === '.circleci/config.yml')
12
+ return 'circleci';
13
+ if (file === 'bitbucket-pipelines.yml')
14
+ return 'bitbucket';
15
+ if (file === 'azure-pipelines.yml')
16
+ return 'azure';
17
+ return undefined;
18
+ }
19
+ const isObj = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
20
+ function strings(value) {
21
+ if (typeof value === 'string')
22
+ return [value];
23
+ if (Array.isArray(value))
24
+ return value.flatMap(strings);
25
+ return [];
26
+ }
27
+ function workingDir(value) {
28
+ return typeof value === 'string' && value.trim() !== '' ? path.posix.normalize(value.trim().replace(/^\.\//, '')) : '.';
29
+ }
30
+ function withCd(command, dir) {
31
+ const match = /^\s*cd\s+([^\s&;|"'$`]+)\s*&&/.exec(command);
32
+ return match?.[1] ? path.posix.normalize(path.posix.join(dir, match[1])) : dir;
33
+ }
34
+ function collect(doc, file, format, repoPath, out) {
35
+ const triggers = new Map();
36
+ const push = (job, command, dir, triggerKey = job) => {
37
+ if (command.trim() === '')
38
+ return;
39
+ const key = triggerKey;
40
+ if (!triggers.has(key))
41
+ triggers.set(key, triggersFor(format, doc, key, repoPath));
42
+ out.push({ file, format, job, command, dir: withCd(command, dir), triggers: triggers.get(key) });
43
+ };
44
+ if (!isObj(doc))
45
+ return;
46
+ if (format === 'github' || format === 'circleci') {
47
+ const jobs = isObj(doc.jobs) ? doc.jobs : {};
48
+ for (const [name, job] of Object.entries(jobs)) {
49
+ if (!isObj(job))
50
+ continue;
51
+ const defaults = isObj(job.defaults) && isObj(job.defaults.run) ? workingDir(job.defaults.run['working-directory']) : '.';
52
+ const steps = Array.isArray(job.steps) ? job.steps : [];
53
+ for (const step of steps) {
54
+ if (format === 'github') {
55
+ if (!isObj(step) || typeof step.run !== 'string')
56
+ continue;
57
+ push(name, step.run, step['working-directory'] === undefined ? defaults : workingDir(step['working-directory']));
58
+ }
59
+ else {
60
+ const run = isObj(step) ? step.run : undefined;
61
+ const command = typeof run === 'string' ? run : isObj(run) && typeof run.command === 'string' ? run.command : undefined;
62
+ if (command !== undefined)
63
+ push(name, command, '.');
64
+ }
65
+ }
66
+ }
67
+ }
68
+ else if (format === 'gitlab') {
69
+ for (const [name, value] of Object.entries(doc)) {
70
+ if (!isObj(value))
71
+ continue;
72
+ for (const key of ['before_script', 'script', 'after_script'])
73
+ for (const command of strings(value[key]))
74
+ push(name, command, '.');
75
+ }
76
+ }
77
+ else if (format === 'bitbucket') {
78
+ const walk = (node, name, section) => {
79
+ if (Array.isArray(node))
80
+ node.forEach((child) => walk(child, name, section));
81
+ else if (isObj(node)) {
82
+ for (const [key, child] of Object.entries(node)) {
83
+ if (key === 'script')
84
+ for (const command of strings(child))
85
+ push(name, command, '.', section);
86
+ else
87
+ walk(child, key === 'name' || typeof child !== 'object' ? name : key, section);
88
+ }
89
+ }
90
+ };
91
+ for (const [section, child] of Object.entries(isObj(doc.pipelines) ? doc.pipelines : {}))
92
+ walk(child, section, section);
93
+ }
94
+ else {
95
+ const walk = (node, name) => {
96
+ if (Array.isArray(node))
97
+ node.forEach((child) => walk(child, name));
98
+ else if (isObj(node)) {
99
+ for (const [key, child] of Object.entries(node)) {
100
+ if (key === 'steps' && Array.isArray(child)) {
101
+ for (const step of child) {
102
+ if (!isObj(step))
103
+ continue;
104
+ for (const field of ['script', 'bash', 'pwsh', 'powershell']) {
105
+ if (typeof step[field] === 'string')
106
+ push(name, step[field], workingDir(step.workingDirectory), '');
107
+ }
108
+ }
109
+ }
110
+ else
111
+ walk(child, typeof node.job === 'string' ? node.job : name);
112
+ }
113
+ }
114
+ };
115
+ walk(doc, 'steps');
116
+ }
117
+ }
118
+ /** Every command of the repository's CI files. Invalid YAML is skipped with a warning. */
119
+ export function ciCommands(repoPath, warnings = []) {
120
+ const out = [];
121
+ for (const file of trackedPaths(repoPath)) {
122
+ const format = ciFormatOf(file);
123
+ if (!format)
124
+ continue;
125
+ const text = readTracked(repoPath, file);
126
+ if (text === null)
127
+ continue;
128
+ let doc;
129
+ try {
130
+ doc = parse(text);
131
+ }
132
+ catch (err) {
133
+ warnings.push(`${file}: invalid YAML, skipped (${err instanceof Error ? err.message.split('\n')[0] : String(err)})`);
134
+ continue;
135
+ }
136
+ collect(doc, file, format, repoPath, out);
137
+ }
138
+ return out;
139
+ }
@@ -0,0 +1,31 @@
1
+ import path from 'node:path';
2
+ import { readTracked } from '../../scope/tracked.js';
3
+ /** The first of `names` that is tracked in `dir`, or in the nearest ancestor when `ancestors` is set. */
4
+ export function findFile(repoPath, dir, names, ancestors) {
5
+ for (let current = path.posix.normalize(dir === '' ? '.' : dir);; current = path.posix.dirname(current)) {
6
+ for (const name of names) {
7
+ const file = path.posix.join(current, name);
8
+ const text = readTracked(repoPath, file);
9
+ if (text !== null)
10
+ return { file, text };
11
+ }
12
+ if (!ancestors || current === '.' || current === '' || current.startsWith('..'))
13
+ return undefined;
14
+ }
15
+ }
16
+ /** Join backslash-continued lines. */
17
+ export function joinContinuations(lines) {
18
+ const out = [];
19
+ let pending = '';
20
+ for (const line of lines) {
21
+ if (line.endsWith('\\')) {
22
+ pending += `${line.slice(0, -1).trimEnd()} `;
23
+ continue;
24
+ }
25
+ out.push((pending + line).trim());
26
+ pending = '';
27
+ }
28
+ if (pending.trim() !== '')
29
+ out.push(pending.trim());
30
+ return out;
31
+ }
@@ -0,0 +1,18 @@
1
+ import { justfileEntryPoints } from './justfile.js';
2
+ import { makefileEntryPoints } from './makefile.js';
3
+ import { noxEntryPoints } from './nox.js';
4
+ import { packageJsonEntryPoints } from './package-json.js';
5
+ import { pyprojectEntryPoints } from './pyproject.js';
6
+ import { toxEntryPoints } from './tox.js';
7
+ /** Explicit list. */
8
+ export const entryPointKinds = [
9
+ packageJsonEntryPoints,
10
+ makefileEntryPoints,
11
+ justfileEntryPoints,
12
+ toxEntryPoints,
13
+ noxEntryPoints,
14
+ pyprojectEntryPoints,
15
+ ];
16
+ export function entryPointKind(id) {
17
+ return entryPointKinds.find((kind) => kind.id === id);
18
+ }
@@ -0,0 +1,62 @@
1
+ import { findFile, joinContinuations } from './find.js';
2
+ const JUSTFILES = ['justfile', 'Justfile', '.justfile'];
3
+ /** Line based: `name [params...]: [dependencies]`, indented lines are the body. `alias`, `set`, `import`, `mod`, assignments and attributes are ignored. */
4
+ function parseJustfile(text) {
5
+ const recipes = new Map();
6
+ const lines = text.replace(/\r\n/g, '\n').split('\n');
7
+ let current;
8
+ let shebang = false;
9
+ for (const line of lines) {
10
+ if (/^[ \t]+\S/.test(line)) {
11
+ if (!current)
12
+ continue;
13
+ const trimmed = line.trim();
14
+ if (current.body.length === 0 && trimmed.startsWith('#!')) {
15
+ shebang = true;
16
+ current.body.push('');
17
+ continue;
18
+ }
19
+ if (shebang)
20
+ current.body[0] = `${current.body[0] ?? ''}\n${trimmed}`.replace(/^\n/, '');
21
+ else
22
+ current.body.push(trimmed.replace(/^[@-]+\s*/, ''));
23
+ continue;
24
+ }
25
+ if (line.trim() === '')
26
+ continue;
27
+ current = undefined;
28
+ shebang = false;
29
+ if (/^\s*#/.test(line) || /^\s*\[/.test(line) || /^(alias|set|import|mod|export)\b/.test(line))
30
+ continue;
31
+ const match = /^@?([A-Za-z_][\w-]*)([^:=]*?):(?!=)\s*(.*)$/.exec(line);
32
+ if (!match?.[1])
33
+ continue;
34
+ const dependencies = (match[3] ?? '')
35
+ .replace(/#.*$/, '')
36
+ .split(/&&|\s+/)
37
+ .map((d) => d.replace(/^\(|\)$/g, '').trim())
38
+ .filter((d) => d !== '' && /^[A-Za-z_][\w-]*$/.test(d));
39
+ current = { dependencies, body: [] };
40
+ recipes.set(match[1], current);
41
+ }
42
+ for (const recipe of recipes.values())
43
+ recipe.body = joinContinuations(recipe.body).filter((l) => l !== '');
44
+ return recipes;
45
+ }
46
+ /** `justfile`, `Justfile` or `.justfile` in `dir`, else the nearest ancestor. Dependencies are expanded like Makefile prerequisites. */
47
+ export const justfileEntryPoints = {
48
+ id: 'justfile',
49
+ list(repoPath, dir) {
50
+ const found = findFile(repoPath, dir, JUSTFILES, false);
51
+ return found ? [...parseJustfile(found.text).keys()].map((name) => ({ name, invoke: `just ${name}` })) : [];
52
+ },
53
+ lookup(repoPath, dir, name) {
54
+ const found = findFile(repoPath, dir, JUSTFILES, true);
55
+ if (!found)
56
+ return undefined;
57
+ const recipes = parseJustfile(found.text);
58
+ const key = name === '@default' ? [...recipes.keys()][0] : name;
59
+ const recipe = key === undefined ? undefined : recipes.get(key);
60
+ return recipe ? { file: found.file, bodies: [...recipe.dependencies.map((d) => `just ${d}`), ...recipe.body] } : undefined;
61
+ },
62
+ };
@@ -0,0 +1,66 @@
1
+ import path from 'node:path';
2
+ import { readTracked } from '../../scope/tracked.js';
3
+ const MAKEFILE_NAMES = ['Makefile', 'makefile', 'GNUmakefile'];
4
+ /** Line based: `name [name...]: [prerequisites]` and tab-indented recipe lines. */
5
+ export function parseMakefile(text) {
6
+ const targets = new Map();
7
+ const lines = text.replace(/\r\n/g, '\n').split('\n');
8
+ let current = [];
9
+ for (let i = 0; i < lines.length; i += 1) {
10
+ let line = lines[i] ?? '';
11
+ if (line.startsWith('\t')) {
12
+ while (line.endsWith('\\') && i + 1 < lines.length) {
13
+ i += 1;
14
+ line = `${line.slice(0, -1).trimEnd()} ${(lines[i] ?? '').trim()}`;
15
+ }
16
+ const command = line.trim().replace(/^[@\-+]+\s*/, '');
17
+ if (command !== '')
18
+ for (const target of current)
19
+ target.recipe.push(command);
20
+ continue;
21
+ }
22
+ current = [];
23
+ const match = /^([^\s#:=][^:=#]*?)\s*:(?![:=])\s*([^#]*)/.exec(line);
24
+ if (!match?.[1] || /^(include|-include|ifeq|ifneq|ifdef|ifndef|else|endif|define|endef|export|override)\b/.test(line))
25
+ continue;
26
+ const names = match[1].split(/\s+/).filter((n) => n !== '' && !n.includes('%') && !n.includes('$') && n !== '.PHONY');
27
+ const prerequisites = (match[2] ?? '').split(/\s+/).filter((n) => n !== '' && !n.includes('$') && !n.includes('%'));
28
+ for (const name of names) {
29
+ const target = targets.get(name) ?? { prerequisites: [], recipe: [] };
30
+ target.prerequisites.push(...prerequisites);
31
+ targets.set(name, target);
32
+ current.push(target);
33
+ }
34
+ }
35
+ return targets;
36
+ }
37
+ /** Targets of the Makefile in `dir`. A prerequisite is expanded like a call to `make <prerequisite>`. */
38
+ export const makefileEntryPoints = {
39
+ id: 'makefile',
40
+ list(repoPath, dir) {
41
+ const base = dir === '' ? '.' : dir;
42
+ for (const candidate of MAKEFILE_NAMES) {
43
+ const text = readTracked(repoPath, path.posix.join(base, candidate));
44
+ if (text === null)
45
+ continue;
46
+ return [...parseMakefile(text).keys()]
47
+ .filter((name) => !name.startsWith('.') && !name.includes('%'))
48
+ .map((name) => ({ name, invoke: `make ${name}` }));
49
+ }
50
+ return [];
51
+ },
52
+ lookup(repoPath, dir, name) {
53
+ const base = dir === '' ? '.' : dir;
54
+ for (const candidate of MAKEFILE_NAMES) {
55
+ const file = path.posix.join(base, candidate);
56
+ const text = readTracked(repoPath, file);
57
+ if (text === null)
58
+ continue;
59
+ const target = parseMakefile(text).get(name);
60
+ if (!target)
61
+ return undefined;
62
+ return { file, bodies: [...target.prerequisites.map((p) => `make ${p}`), ...target.recipe] };
63
+ }
64
+ return undefined;
65
+ },
66
+ };
@@ -0,0 +1,29 @@
1
+ import path from 'node:path';
2
+ import { noxSessions } from '../../python/nox.js';
3
+ import { trackedPaths } from '../../scope/tracked.js';
4
+ function noxfileIn(repoPath, dir) {
5
+ const file = path.posix.join(dir === '' ? '.' : dir, 'noxfile.py');
6
+ return trackedPaths(repoPath).includes(file) ? file : undefined;
7
+ }
8
+ /** `noxfile.py` in `dir`. A call is `nox [-s <sessions>]`; without `-s` every session runs. */
9
+ export const noxEntryPoints = {
10
+ id: 'nox',
11
+ fileFor: noxfileIn,
12
+ list(repoPath, dir) {
13
+ const file = noxfileIn(repoPath, dir);
14
+ const sessions = file ? noxSessions(repoPath, file) : undefined;
15
+ return (sessions ?? []).map((s) => ({ name: s.name, invoke: `nox -s ${s.name}` }));
16
+ },
17
+ lookup(repoPath, dir, name) {
18
+ const file = noxfileIn(repoPath, dir);
19
+ const sessions = file ? noxSessions(repoPath, file) : undefined;
20
+ if (!file || !sessions)
21
+ return undefined;
22
+ if (name === '@all')
23
+ return { file, bodies: sessions.flatMap((s) => s.commands) };
24
+ const session = sessions.find((s) => s.name === name);
25
+ if (!session)
26
+ return undefined;
27
+ return { file, bodies: [...session.notify.map((n) => `nox -s ${n}`), ...session.commands] };
28
+ },
29
+ };
@@ -0,0 +1,21 @@
1
+ import path from 'node:path';
2
+ import { readPackageJson } from '../../manifest/package-json.js';
3
+ /** `scripts` of the `package.json` in `dir`, else the nearest ancestor within the repository. */
4
+ export const packageJsonEntryPoints = {
5
+ id: 'package-json',
6
+ list(repoPath, dir) {
7
+ const manifest = readPackageJson(repoPath, dir);
8
+ return Object.keys(manifest?.scripts ?? {}).map((name) => ({ name, invoke: `npm run ${name}` }));
9
+ },
10
+ lookup(repoPath, dir, name) {
11
+ for (let current = path.posix.normalize(dir === '' ? '.' : dir);; current = path.posix.dirname(current)) {
12
+ const manifest = readPackageJson(repoPath, current);
13
+ if (manifest) {
14
+ const body = manifest.scripts[name];
15
+ return body === undefined ? undefined : { file: path.posix.join(current, 'package.json'), bodies: [body] };
16
+ }
17
+ if (current === '.' || current === '' || current.startsWith('..'))
18
+ return undefined;
19
+ }
20
+ },
21
+ };