vibecodedshit 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/LICENSE +95 -0
  2. package/README.md +73 -2
  3. package/assets/fonts/NotoEmoji-OFL.txt +93 -0
  4. package/assets/fonts/NotoEmoji-Poop.ttf +0 -0
  5. package/assets/fonts/SpaceGrotesk-Bold.ttf +0 -0
  6. package/assets/fonts/SpaceGrotesk-OFL.txt +93 -0
  7. package/dist/bin.js +11 -0
  8. package/dist/card/previous.js +38 -0
  9. package/dist/card/render.js +71 -0
  10. package/dist/cli.js +138 -0
  11. package/dist/commands/card.js +69 -0
  12. package/dist/commands/common.js +1 -0
  13. package/dist/commands/diff.js +54 -0
  14. package/dist/commands/footer.js +28 -0
  15. package/dist/commands/report.js +49 -0
  16. package/dist/commands/route.js +62 -0
  17. package/dist/commands/submit.js +125 -0
  18. package/dist/commands/summary.js +77 -0
  19. package/dist/diff/compare.js +73 -0
  20. package/dist/diff/conditions.js +87 -0
  21. package/dist/diff/format.js +23 -0
  22. package/dist/exec/docker.js +158 -0
  23. package/dist/exec/findings.js +73 -0
  24. package/dist/exec/plan.js +50 -0
  25. package/dist/exec/reports.js +90 -0
  26. package/dist/exec/roots.js +49 -0
  27. package/dist/exec/run.js +182 -0
  28. package/dist/exec/steps/build.js +45 -0
  29. package/dist/exec/steps/coverage-install.js +12 -0
  30. package/dist/exec/steps/install.js +41 -0
  31. package/dist/exec/steps/knip.js +28 -0
  32. package/dist/exec/steps/lint.js +25 -0
  33. package/dist/exec/steps/lockfiles.js +25 -0
  34. package/dist/exec/steps/test.js +17 -0
  35. package/dist/exec/steps/typecheck.js +40 -0
  36. package/dist/exec/test-reports/coverage-py.js +23 -0
  37. package/dist/exec/test-reports/coverage-ranges.js +65 -0
  38. package/dist/exec/test-reports/coverage-summary.js +23 -0
  39. package/dist/exec/test-reports/jest-json.js +31 -0
  40. package/dist/exec/test-reports/junit.js +24 -0
  41. package/dist/exec/test-reports/mocha-json.js +18 -0
  42. package/dist/exec/test-reports/vitest-json.js +5 -0
  43. package/dist/exec/test-runners/detect.js +99 -0
  44. package/dist/exec/test-runners/index.js +6 -0
  45. package/dist/exec/test-runners/jest.js +27 -0
  46. package/dist/exec/test-runners/mocha.js +28 -0
  47. package/dist/exec/test-runners/node-test.js +24 -0
  48. package/dist/exec/test-runners/pytest.js +18 -0
  49. package/dist/exec/test-runners/script.js +48 -0
  50. package/dist/exec/test-runners/spec.js +1 -0
  51. package/dist/exec/test-runners/vitest.js +28 -0
  52. package/dist/exec/tests.js +120 -0
  53. package/dist/exec/types.js +1 -0
  54. package/dist/interactive/after-scan.js +121 -0
  55. package/dist/interactive/docker-setup.js +97 -0
  56. package/dist/interactive/prompt.js +17 -0
  57. package/dist/interactive/system.js +10 -0
  58. package/dist/manifest/package-json.js +36 -0
  59. package/dist/manifest/python.js +106 -0
  60. package/dist/measure/d3/index.js +45 -0
  61. package/dist/measure/d3/presets.js +100 -0
  62. package/dist/measure/d3/py/adapter.js +67 -0
  63. package/dist/measure/d3/py/config.js +80 -0
  64. package/dist/measure/d3/py/contracts.js +57 -0
  65. package/dist/measure/d3/py/domain.js +57 -0
  66. package/dist/measure/d3/py/graph.js +214 -0
  67. package/dist/measure/d3/py/interpreter.js +31 -0
  68. package/dist/measure/d3/py/layers.js +120 -0
  69. package/dist/measure/d3/py/roots.js +58 -0
  70. package/dist/measure/d3/py/types.js +1 -0
  71. package/dist/measure/d3/scc.js +64 -0
  72. package/dist/measure/d3/ts/adapter.js +371 -0
  73. package/dist/measure/d3/ts/classify.js +107 -0
  74. package/dist/measure/d3/ts/config.js +318 -0
  75. package/dist/measure/d3/ts/domain.js +23 -0
  76. package/dist/measure/d3/ts/rules.js +65 -0
  77. package/dist/measure/d3/types.js +5 -0
  78. package/dist/measure/deps/cvss.js +66 -0
  79. package/dist/measure/deps/declared.js +195 -0
  80. package/dist/measure/deps/files.js +40 -0
  81. package/dist/measure/deps/lockfiles.js +308 -0
  82. package/dist/measure/deps/osv.js +122 -0
  83. package/dist/measure/deps/registry.js +142 -0
  84. package/dist/measure/deps/resolved.js +78 -0
  85. package/dist/measure/deps/units.js +5 -0
  86. package/dist/measure/eslint/health-plugin.js +19 -0
  87. package/dist/measure/ops/docs-commands.js +73 -0
  88. package/dist/measure/ops/docs-resolve.js +217 -0
  89. package/dist/measure/ops/entry-points.js +49 -0
  90. package/dist/measure/ops/env-example.js +20 -0
  91. package/dist/measure/ops/env-reads.js +46 -0
  92. package/dist/measure/ops/runtime.js +73 -0
  93. package/dist/measure/p1-1.js +8 -0
  94. package/dist/measure/p1-2.js +8 -0
  95. package/dist/measure/p1-3.js +8 -0
  96. package/dist/measure/p1-4.js +41 -0
  97. package/dist/measure/p1-5.js +59 -0
  98. package/dist/measure/p2-1.js +8 -0
  99. package/dist/measure/p2-2.js +44 -0
  100. package/dist/measure/p2-3.js +81 -0
  101. package/dist/measure/p2-4.js +288 -0
  102. package/dist/measure/p2-5.js +8 -0
  103. package/dist/measure/p3-1.js +16 -0
  104. package/dist/measure/p3-2.js +26 -0
  105. package/dist/measure/p3-3.js +37 -0
  106. package/dist/measure/p3-4.js +26 -0
  107. package/dist/measure/p3-5.js +26 -0
  108. package/dist/measure/p4-1.js +184 -0
  109. package/dist/measure/p4-2.js +27 -0
  110. package/dist/measure/p4-3.js +106 -0
  111. package/dist/measure/p4-4.js +309 -0
  112. package/dist/measure/p5-1.js +117 -0
  113. package/dist/measure/p5-2.js +80 -0
  114. package/dist/measure/p5-3.js +21 -0
  115. package/dist/measure/p5-4.js +161 -0
  116. package/dist/measure/p5-5.js +158 -0
  117. package/dist/measure/p6-1.js +20 -0
  118. package/dist/measure/p6-2.js +20 -0
  119. package/dist/measure/p6-3.js +40 -0
  120. package/dist/measure/p6-4.js +31 -0
  121. package/dist/measure/p6-5.js +60 -0
  122. package/dist/measure/p6-6.js +54 -0
  123. package/dist/measure/py/typecheck-config.js +95 -0
  124. package/dist/measure/registry.js +31 -0
  125. package/dist/measure/safety/py-tests.js +124 -0
  126. package/dist/measure/safety/ts-barrels.js +68 -0
  127. package/dist/measure/safety/ts-cases.js +111 -0
  128. package/dist/measure/safety/ts-credits.js +101 -0
  129. package/dist/measure/safety/ts-imports.js +63 -0
  130. package/dist/measure/safety/ts-resolve.js +43 -0
  131. package/dist/measure/ts/directives.js +37 -0
  132. package/dist/measure/ts/exports.js +60 -0
  133. package/dist/measure/ts/tsconfig.js +179 -0
  134. package/dist/measure/types.js +1 -0
  135. package/dist/measure/units.js +41 -0
  136. package/dist/methodology/load.js +159 -0
  137. package/dist/methodology/schema.js +105 -0
  138. package/dist/python/deadcode.js +27 -0
  139. package/dist/python/env.js +13 -0
  140. package/dist/python/helper.js +24 -0
  141. package/dist/python/imports.js +40 -0
  142. package/dist/python/nox.js +16 -0
  143. package/dist/python/scope.js +35 -0
  144. package/dist/python/tests.js +19 -0
  145. package/dist/python/typing.js +27 -0
  146. package/dist/report/escape.js +5 -0
  147. package/dist/report/format.js +75 -0
  148. package/dist/report/render.js +213 -0
  149. package/dist/report/roadmap.js +58 -0
  150. package/dist/report/scares.js +83 -0
  151. package/dist/report/style.js +27 -0
  152. package/dist/report-only/hotspots.js +48 -0
  153. package/dist/route/check.js +37 -0
  154. package/dist/route/files.js +74 -0
  155. package/dist/route/order.js +22 -0
  156. package/dist/route/select.js +114 -0
  157. package/dist/route/write.js +13 -0
  158. package/dist/run/read.js +89 -0
  159. package/dist/run/recompute.js +19 -0
  160. package/dist/run/scan.js +196 -0
  161. package/dist/run/schema.js +1 -0
  162. package/dist/run/score-run.js +42 -0
  163. package/dist/run/write.js +4 -0
  164. package/dist/scope/build.js +173 -0
  165. package/dist/scope/packages.js +87 -0
  166. package/dist/scope/rules.js +21 -0
  167. package/dist/scope/tracked.js +34 -0
  168. package/dist/scope/ts-lines.js +63 -0
  169. package/dist/scope/types.js +7 -0
  170. package/dist/scoring/aggregate.js +38 -0
  171. package/dist/scoring/pool.js +65 -0
  172. package/dist/scoring/score.js +81 -0
  173. package/dist/scoring/what-if.js +38 -0
  174. package/dist/scripts/checks.js +53 -0
  175. package/dist/scripts/ci.js +139 -0
  176. package/dist/scripts/entry-points/find.js +31 -0
  177. package/dist/scripts/entry-points/index.js +18 -0
  178. package/dist/scripts/entry-points/justfile.js +62 -0
  179. package/dist/scripts/entry-points/makefile.js +66 -0
  180. package/dist/scripts/entry-points/nox.js +29 -0
  181. package/dist/scripts/entry-points/package-json.js +21 -0
  182. package/dist/scripts/entry-points/pyproject.js +73 -0
  183. package/dist/scripts/entry-points/tox.js +130 -0
  184. package/dist/scripts/expand.js +300 -0
  185. package/dist/scripts/triggers.js +151 -0
  186. package/dist/scripts/types.js +1 -0
  187. package/dist/submit/constants.js +10 -0
  188. package/dist/submit/payload.js +42 -0
  189. package/dist/submit/repo-identity.js +46 -0
  190. package/dist/submit/send.js +29 -0
  191. package/dist/tools/catalog.js +46 -0
  192. package/dist/tools/image.js +180 -0
  193. package/dist/tools/kit.js +71 -0
  194. package/dist/tools/knip.js +169 -0
  195. package/dist/tools/outcome.js +21 -0
  196. package/dist/tools/resolve.js +66 -0
  197. package/dist/tools/workcopy.js +27 -0
  198. package/dist/util/ini.js +37 -0
  199. package/dist/util/pep440.js +163 -0
  200. package/dist/util/toml.js +27 -0
  201. package/docker/Dockerfile +142 -0
  202. package/docker/bin/health-capture +5 -0
  203. package/docker/bin/health-coverage-install +5 -0
  204. package/docker/bin/health-eslint +66 -0
  205. package/docker/bin/health-pytest +10 -0
  206. package/docker/bin/health-tsc +10 -0
  207. package/docker/bin/health-venv +11 -0
  208. package/docker/config/eslint.config.mjs +4 -0
  209. package/docker/config/eslint.rules.mjs +65 -0
  210. package/docker/config/eslint.untyped.config.mjs +4 -0
  211. package/docker/config/knip.json +1 -0
  212. package/docker/config/ruff.toml +7 -0
  213. package/methodology/LICENSE +396 -0
  214. package/methodology/v1.0/METHODOLOGY.md +1176 -0
  215. package/methodology/v1.0/dimensions/1-safety-net.yaml +240 -0
  216. package/methodology/v1.0/dimensions/2-type-static-rigor.yaml +289 -0
  217. package/methodology/v1.0/dimensions/3-architecture-boundaries.yaml +266 -0
  218. package/methodology/v1.0/dimensions/4-complexity-maintainability.yaml +192 -0
  219. package/methodology/v1.0/dimensions/5-dependency-security-hygiene.yaml +223 -0
  220. package/methodology/v1.0/dimensions/6-operability-feedback-loop.yaml +279 -0
  221. package/methodology/v1.0/methodology.yaml +242 -0
  222. package/methodology/v1.0/presets/django.yaml +43 -0
  223. package/methodology/v1.0/presets/express.yaml +47 -0
  224. package/methodology/v1.0/presets/fastapi.yaml +52 -0
  225. package/methodology/v1.0/presets/generic.yaml +41 -0
  226. package/methodology/v1.0/presets/nextjs.yaml +50 -0
  227. package/methodology/v1.0/scope.yaml +115 -0
  228. package/package.json +61 -4
  229. package/python/dead_code_helper.py +265 -0
  230. package/python/env_helper.py +71 -0
  231. package/python/graph_helper.py +277 -0
  232. package/python/imports_helper.py +132 -0
  233. package/python/nox_helper.py +79 -0
  234. package/python/scope_helper.py +96 -0
  235. package/python/tests_helper.py +171 -0
  236. package/python/typing_helper.py +235 -0
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The fixed order Slices are written in (ADR 0001): secrets and known vulnerabilities, then the repo installs, builds
3
+ * and verifies, then the safety net, then types and lint, then CI and docs. Structural refactors and major upgrades
4
+ * (3.x, 4.x, 5.2) come last. It is advice about working order, so it lives here and not in the methodology YAML.
5
+ */
6
+ const PRECEDENCE = [
7
+ '5.4', '5.1',
8
+ '6.1', '5.3', '5.5', '6.2', '6.5',
9
+ '1.1', '1.4', '1.2', '1.3', '1.5',
10
+ '2.1', '2.2', '2.5', '2.3', '2.4',
11
+ '6.4', '6.6', '6.3',
12
+ ];
13
+ const LAST_GROUPS = ['3', '4'];
14
+ /** Lower ranks are written first. Parameters not in the list go last: 3.x, then 4.x, then everything else (5.2). */
15
+ export function precedenceRank(id) {
16
+ const listed = PRECEDENCE.indexOf(id);
17
+ if (listed >= 0)
18
+ return listed;
19
+ const [dimension = '', index = '0'] = id.split('.');
20
+ const group = LAST_GROUPS.indexOf(dimension);
21
+ return PRECEDENCE.length + (group >= 0 ? group : LAST_GROUPS.length) * 100 + Number(index);
22
+ }
@@ -0,0 +1,114 @@
1
+ import { roundHalfUp } from '../scoring/aggregate.js';
2
+ import { healthWith, nextCandidate } from '../scoring/what-if.js';
3
+ import { precedenceRank } from './order.js';
4
+ export const PREVIEW_LINE = 'Install Docker for a full run to get your Route';
5
+ /** Vibe-coded and Fragile aim at Stable, Stable at Agent-ready, Agent-ready at nothing. The bounds come from the YAML. */
6
+ export function targetBand(current, methodology) {
7
+ const bands = [...methodology.bands].sort((a, b) => a.min - b.min);
8
+ const stable = bands.findIndex((b) => b.key === 'stable');
9
+ const at = bands.findIndex((b) => b.key === current);
10
+ if (stable < 0 || at < 0 || at > stable)
11
+ return null;
12
+ return at < stable ? (bands[stable] ?? null) : (bands[stable + 1] ?? null);
13
+ }
14
+ function currentScore(run, id, overrides) {
15
+ const override = overrides.get(id);
16
+ if (override !== undefined)
17
+ return override;
18
+ const param = run.parameters.find((p) => p.id === id);
19
+ if (param?.status === 'failed')
20
+ return 0;
21
+ if (param?.status === 'measured')
22
+ return param.score ?? 0;
23
+ return null;
24
+ }
25
+ /** The greedy Steps: each is the one that raises the displayed health most, until the Target band is reached or no Step adds health. */
26
+ function selectSteps(run, methodology, target) {
27
+ const overrides = new Map();
28
+ const steps = [];
29
+ for (;;) {
30
+ const base = healthWith(run, methodology, overrides);
31
+ if (base.health === null)
32
+ return { steps, reached: false };
33
+ const displayed = roundHalfUp(base.health);
34
+ if (displayed >= target.min)
35
+ return { steps, reached: true };
36
+ let best = null;
37
+ for (const dim of methodology.dimensions) {
38
+ for (const def of dim.parameters) {
39
+ const score = currentScore(run, def.id, overrides);
40
+ if (score === null)
41
+ continue;
42
+ const candidate = nextCandidate(def, score);
43
+ if (!candidate)
44
+ continue;
45
+ const after = healthWith(run, methodology, new Map([...overrides, [def.id, candidate.score]])).health;
46
+ if (after === null || after - base.health <= 1e-9)
47
+ continue;
48
+ const entry = {
49
+ step: { id: def.id, name: def.name, dimensionId: dim.id, fromScore: score, toScore: candidate.score, descriptor: candidate.descriptor },
50
+ rise: roundHalfUp(after) - displayed,
51
+ precise: after - base.health,
52
+ dimensionScore: base.dimension.get(dim.id) ?? 0,
53
+ };
54
+ if (!best ||
55
+ entry.rise > best.rise ||
56
+ (entry.rise === best.rise &&
57
+ (entry.precise > best.precise + 1e-9 ||
58
+ (Math.abs(entry.precise - best.precise) <= 1e-9 &&
59
+ (entry.dimensionScore < best.dimensionScore || (entry.dimensionScore === best.dimensionScore && entry.step.id.localeCompare(best.step.id, 'en', { numeric: true }) < 0)))))) {
60
+ best = entry;
61
+ }
62
+ }
63
+ }
64
+ if (!best)
65
+ return { steps, reached: false };
66
+ overrides.set(best.step.id, best.step.toScore);
67
+ steps.push(best.step);
68
+ }
69
+ }
70
+ /** The Route for a run: Steps chosen by Gain, then written as Slices in the fixed precedence with the health after each. */
71
+ export function buildRoute(run, methodology) {
72
+ if (!run.health)
73
+ return { kind: 'none' };
74
+ const target = targetBand(run.health.band, methodology);
75
+ if (!target)
76
+ return { kind: 'none' };
77
+ if (run.mode === 'preview')
78
+ return { kind: 'preview' };
79
+ const { steps, reached } = selectSteps(run, methodology, target);
80
+ const ordered = [...steps].sort((a, b) => precedenceRank(a.id) - precedenceRank(b.id) || a.toScore - b.toScore);
81
+ const overrides = new Map();
82
+ let before = run.health.displayed;
83
+ const slices = ordered.map((step, i) => {
84
+ overrides.set(step.id, step.toScore);
85
+ const after = roundHalfUp(healthWith(run, methodology, overrides).health ?? before);
86
+ const slice = { ...step, position: i + 1, healthBefore: before, healthAfter: after };
87
+ before = after;
88
+ return slice;
89
+ });
90
+ return {
91
+ kind: 'route',
92
+ target,
93
+ from: run.health.displayed,
94
+ to: before,
95
+ reached,
96
+ unmeasured: run.parameters.filter((p) => p.status === 'not_measured').map((p) => p.id),
97
+ slices,
98
+ };
99
+ }
100
+ /** The stdout lines: the summary, plus the out-of-reach line when the Target band is not reached. A preview gets its one line, Agent-ready nothing. */
101
+ export function routeLines(plan) {
102
+ if (plan.kind === 'preview')
103
+ return [PREVIEW_LINE];
104
+ if (plan.kind === 'none')
105
+ return [];
106
+ const lines = [`Route to ${plan.target.name}: ${plan.slices.length} Slices, ${plan.from} -> ${plan.to}`];
107
+ if (!plan.reached)
108
+ lines.push(outOfReachLine(plan));
109
+ return lines;
110
+ }
111
+ export function outOfReachLine(plan) {
112
+ const reason = plan.unmeasured.length > 0 ? `until ${plan.unmeasured.join(', ')} are measured` : 'with the parameters measured, because no further Step adds health';
113
+ return `${plan.target.name} is out of reach ${reason}; this Route gets you to ${plan.to}`;
114
+ }
@@ -0,0 +1,13 @@
1
+ import { mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { ROUTE_FILE, SLICE_FILE } from './files.js';
4
+ /** Deletes the earlier ROUTE.md and Slice files in `dir` (nothing else), then writes the new ones. */
5
+ export function writeRouteFiles(dir, files) {
6
+ mkdirSync(dir, { recursive: true });
7
+ for (const name of readdirSync(dir)) {
8
+ if (name === ROUTE_FILE || SLICE_FILE.test(name))
9
+ rmSync(path.join(dir, name), { force: true });
10
+ }
11
+ for (const file of files)
12
+ writeFileSync(path.join(dir, file.name), file.content, 'utf8');
13
+ }
@@ -0,0 +1,89 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { z } from 'zod';
3
+ export class RunReadError extends Error {
4
+ constructor(message) {
5
+ super(message);
6
+ this.name = 'RunReadError';
7
+ }
8
+ }
9
+ const nullableScore = z.number().nullable();
10
+ /** The fields `report` and `diff` read. Unknown fields pass through untouched. */
11
+ const runFileSchema = z.looseObject({
12
+ schemaVersion: z.literal(1),
13
+ valid: z.boolean(),
14
+ invalidReason: z.string().optional(),
15
+ warnings: z.array(z.string()),
16
+ methodology: z.looseObject({ version: z.string() }),
17
+ mode: z.enum(['preview', 'full']),
18
+ comparable: z.boolean(),
19
+ comparableReason: z.string().optional(),
20
+ asOf: z.string(),
21
+ layers: z.array(z.enum(['static', 'execution'])),
22
+ repo: z.looseObject({ path: z.string(), headCommit: z.string().nullable(), treeHash: z.string().nullable() }),
23
+ tools: z.record(z.string(), z.unknown()),
24
+ execution: z
25
+ .looseObject({
26
+ feedbackLoop: z.looseObject({ typecheckSeconds: z.number(), lintSeconds: z.number(), testSeconds: z.number(), totalSeconds: z.number() }),
27
+ roots: z
28
+ .array(z.looseObject({
29
+ dir: z.string(),
30
+ group: z.string(),
31
+ install: z.enum(['ok', 'failed', 'timeout']).nullable(),
32
+ exitCode: z.number().nullable(),
33
+ }))
34
+ .optional(),
35
+ })
36
+ .optional(),
37
+ reportOnly: z.record(z.string(), z.unknown()).optional(),
38
+ ignoreSet: z.looseObject({
39
+ healthignoreSha256: z.string().nullable(),
40
+ gitleaksignoreSha256: z.string().nullable(),
41
+ excludedLinesPercent: z.number(),
42
+ }),
43
+ scope: z.looseObject({ logicFiles: z.number(), logicLines: z.number(), sourceLines: z.number(), packages: z.array(z.string()) }),
44
+ parameters: z.array(z.looseObject({
45
+ id: z.string(),
46
+ status: z.enum(['measured', 'not_applicable', 'not_measured', 'failed']),
47
+ reason: z.string().optional(),
48
+ weight: z.number(),
49
+ raw: z.looseObject({ kind: z.enum(['ratio', 'level', 'number', 'components', 'failed']) }).nullable().optional(),
50
+ score: z.number().optional(),
51
+ units: z.array(z.looseObject({ package: z.string(), group: z.string(), weight: z.number(), raw: z.looseObject({}), score: z.number() })).optional(),
52
+ })),
53
+ dimensions: z.array(z.looseObject({ id: z.number(), key: z.string(), score: nullableScore })),
54
+ breakdown: z.looseObject({
55
+ byPackage: z.record(z.string(), z.record(z.string(), nullableScore)),
56
+ byGroup: z.record(z.string(), z.record(z.string(), nullableScore)),
57
+ }),
58
+ health: z.looseObject({ score: z.number(), displayed: z.number(), band: z.string() }).nullable(),
59
+ });
60
+ /** Parse a run.json document. Throws RunReadError naming the file for anything but a valid version 1 run. */
61
+ export function parseRun(text, file) {
62
+ let doc;
63
+ try {
64
+ doc = JSON.parse(text);
65
+ }
66
+ catch (err) {
67
+ throw new RunReadError(`${file}: not valid JSON (${err instanceof Error ? err.message : String(err)})`);
68
+ }
69
+ if (typeof doc === 'object' && doc !== null && 'schemaVersion' in doc && doc.schemaVersion !== 1) {
70
+ throw new RunReadError(`${file}: unsupported schemaVersion ${JSON.stringify(doc.schemaVersion)} (this CLI reads 1)`);
71
+ }
72
+ const result = runFileSchema.safeParse(doc);
73
+ if (!result.success) {
74
+ const issue = result.error.issues[0];
75
+ const where = issue && issue.path.length > 0 ? issue.path.join('.') : '(root)';
76
+ throw new RunReadError(`${file}: not a run.json: ${where}: ${issue?.message ?? 'invalid'}`);
77
+ }
78
+ return result.data;
79
+ }
80
+ export function readRun(file) {
81
+ let text;
82
+ try {
83
+ text = readFileSync(file, 'utf8');
84
+ }
85
+ catch (err) {
86
+ throw new RunReadError(`${file}: cannot read (${err instanceof Error ? err.message : String(err)})`);
87
+ }
88
+ return parseRun(text, file);
89
+ }
@@ -0,0 +1,19 @@
1
+ import { poolParameter } from '../scoring/pool.js';
2
+ import { dimensionScores, healthFrom } from './score-run.js';
3
+ /** Re-derive every score from the stored raw values. */
4
+ export function recompute(run, methodology) {
5
+ const defs = new Map(methodology.dimensions.flatMap((d) => d.parameters).map((p) => [p.id, p]));
6
+ const parameters = run.parameters.map((param) => {
7
+ const def = defs.get(param.id);
8
+ if (!def)
9
+ throw new Error(`run.json names unknown parameter ${param.id}`);
10
+ if (param.status === 'failed')
11
+ return { ...param, score: 0 };
12
+ if (param.status !== 'measured')
13
+ return param;
14
+ const pooled = poolParameter(def, param.units ?? []);
15
+ return { ...param, raw: pooled.raw, score: pooled.score, units: pooled.units };
16
+ });
17
+ const dimensions = dimensionScores(methodology, parameters);
18
+ return { parameters, dimensions, health: healthFrom(methodology, dimensions) };
19
+ }
@@ -0,0 +1,196 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import ts from 'typescript';
5
+ import { createDockerRunner, imageDigest } from '../exec/docker.js';
6
+ import { runExecution } from '../exec/run.js';
7
+ import { executionRoots, failedRootDirs } from '../exec/roots.js';
8
+ import { feedbackLoop } from '../measure/p6-3.js';
9
+ import { measurers as allMeasurers } from '../measure/registry.js';
10
+ import { computeHotspots } from '../report-only/hotspots.js';
11
+ import { loadMethodology, packageRoot } from '../methodology/load.js';
12
+ import { buildScope } from '../scope/build.js';
13
+ import { poolParameter } from '../scoring/pool.js';
14
+ import { createImageToolKit } from '../tools/image.js';
15
+ import { createToolKit } from '../tools/kit.js';
16
+ import { breakdownFor, dimensionScores, healthFrom } from './score-run.js';
17
+ export class NoLogicFilesError extends Error {
18
+ constructor() {
19
+ super('The repository has no logic file, so it is not scored');
20
+ this.name = 'NoLogicFilesError';
21
+ }
22
+ }
23
+ function git(repoPath, args) {
24
+ const result = spawnSync('git', ['-C', repoPath, ...args], { encoding: 'utf8' });
25
+ return result.error || result.status !== 0 ? null : result.stdout.trim();
26
+ }
27
+ function healthVersion() {
28
+ const pkg = JSON.parse(readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
29
+ return pkg.version;
30
+ }
31
+ function baseRun(opts) {
32
+ const repoPath = path.resolve(opts.repoPath);
33
+ const gitVersion = spawnSync('git', ['--version'], { encoding: 'utf8' }).stdout.trim().replace(/^git version /, '');
34
+ return {
35
+ schemaVersion: 1,
36
+ valid: true,
37
+ warnings: [],
38
+ methodology: { version: opts.methodologyVersion },
39
+ mode: opts.mode,
40
+ comparable: false,
41
+ comparableReason: 'preview, not comparable',
42
+ asOf: opts.asOf,
43
+ layers: ['static'],
44
+ repo: {
45
+ path: repoPath,
46
+ headCommit: git(repoPath, ['rev-parse', '--verify', '-q', 'HEAD']),
47
+ treeHash: git(repoPath, ['rev-parse', '--verify', '-q', 'HEAD^{tree}']),
48
+ },
49
+ tools: { health: healthVersion(), node: process.version, typescript: ts.version, git: gitVersion },
50
+ };
51
+ }
52
+ /** A run for a tool error: marked invalid, with no scores and no health. */
53
+ export function invalidRun(opts, reason) {
54
+ return {
55
+ ...baseRun(opts),
56
+ valid: false,
57
+ invalidReason: reason,
58
+ ignoreSet: { healthignoreSha256: null, gitleaksignoreSha256: null, excludedLinesPercent: 0 },
59
+ scope: { logicFiles: 0, logicLines: 0, sourceLines: 0, packages: [] },
60
+ parameters: [],
61
+ dimensions: [],
62
+ breakdown: { byPackage: {}, byGroup: {} },
63
+ health: null,
64
+ };
65
+ }
66
+ export async function runScan(opts, preloaded) {
67
+ const methodology = preloaded ?? loadMethodology(opts.methodologyVersion, opts.rootDir);
68
+ const repoPath = path.resolve(opts.repoPath);
69
+ const scope = await buildScope(repoPath, methodology);
70
+ if (scope.logicFiles === 0)
71
+ throw new NoLogicFilesError();
72
+ let exec;
73
+ let cleanup;
74
+ if (opts.mode === 'full') {
75
+ const tag = methodology.execution.runner.image;
76
+ ({ results: exec, cleanup } = await runExecution({
77
+ repoPath,
78
+ methodology,
79
+ scope,
80
+ runner: opts.execRunner ?? createDockerRunner(methodology.execution.runner, opts.dockerSpawn),
81
+ image: { tag, digest: opts.imageDigest ?? (await imageDigest(tag, opts.dockerSpawn)) },
82
+ }));
83
+ }
84
+ try {
85
+ return await measureAll(opts, methodology, repoPath, scope, exec);
86
+ }
87
+ finally {
88
+ await cleanup?.();
89
+ }
90
+ }
91
+ /** Every measurer, then scoring. The execution work copy (`exec.workDir`) still exists while this runs. */
92
+ async function measureAll(opts, methodology, repoPath, scope, exec) {
93
+ const tools = opts.tools
94
+ ?? (opts.mode === 'full'
95
+ ? createImageToolKit({ runner: methodology.execution.runner, repoPath, ...(opts.dockerSpawn ? { spawn: opts.dockerSpawn } : {}) })
96
+ : createToolKit());
97
+ const warnings = [...scope.warnings];
98
+ const reportOnly = {};
99
+ const parameters = [];
100
+ for (const dimension of methodology.dimensions) {
101
+ for (const def of dimension.parameters) {
102
+ if (def.layer === 'execution' && opts.mode === 'preview') {
103
+ parameters.push({
104
+ id: def.id,
105
+ status: 'not_measured',
106
+ reason: 'execution layer, not run in preview',
107
+ weight: def.weight,
108
+ });
109
+ continue;
110
+ }
111
+ const measurer = (opts.measurers ?? allMeasurers).find((m) => m.id === def.id);
112
+ if (!measurer) {
113
+ parameters.push({ id: def.id, status: 'not_measured', reason: 'not implemented yet', weight: def.weight });
114
+ continue;
115
+ }
116
+ const measurement = await measurer.measure({
117
+ methodology,
118
+ param: def,
119
+ scope,
120
+ repoPath,
121
+ asOf: opts.asOf,
122
+ mode: opts.mode,
123
+ tools,
124
+ ...(exec ? { exec } : {}),
125
+ });
126
+ if (measurement.status === 'measured') {
127
+ Object.assign(reportOnly, measurement.reportOnly);
128
+ warnings.push(...(measurement.warnings ?? []));
129
+ const pooled = poolParameter(def, measurement.units);
130
+ const failedDirs = exec ? failedRootDirs(exec, scope, measurement.units) : [];
131
+ parameters.push({
132
+ id: def.id,
133
+ status: 'measured',
134
+ ...(failedDirs.length > 0 ? { reason: `install failed in ${failedDirs.join(', ')}` } : {}),
135
+ weight: def.weight,
136
+ raw: pooled.raw,
137
+ score: pooled.score,
138
+ units: pooled.units,
139
+ });
140
+ }
141
+ else if (measurement.status === 'not_measured') {
142
+ parameters.push({ id: def.id, status: 'not_measured', reason: measurement.reason, weight: def.weight });
143
+ }
144
+ else if (measurement.status === 'failed') {
145
+ parameters.push({ id: def.id, status: 'failed', reason: measurement.reason, weight: def.weight, score: 0 });
146
+ }
147
+ else {
148
+ parameters.push({ id: def.id, status: 'not_applicable', reason: measurement.reason, weight: def.weight });
149
+ }
150
+ }
151
+ }
152
+ const complexityByFile = reportOnly.complexityByFile;
153
+ if (Array.isArray(complexityByFile)) {
154
+ const hotspots = computeHotspots(repoPath, opts.asOf, complexityByFile);
155
+ reportOnly.hotspots = hotspots.hotspots;
156
+ warnings.push(...hotspots.warnings);
157
+ }
158
+ reportOnly.pythonInScope = scope.files.some((f) => f.language === 'python');
159
+ const dimensions = dimensionScores(methodology, parameters);
160
+ const base = baseRun(opts);
161
+ const unmeasured = parameters.filter((p) => p.status === 'not_measured').map((p) => `${p.id} (${p.reason})`);
162
+ return {
163
+ ...base,
164
+ ...(opts.mode === 'full'
165
+ ? {
166
+ layers: ['static', 'execution'],
167
+ comparable: unmeasured.length === 0,
168
+ comparableReason: unmeasured.length === 0 ? 'all parameters measured' : `not comparable, not measured: ${unmeasured.join(', ')}`,
169
+ }
170
+ : {}),
171
+ warnings,
172
+ tools: {
173
+ ...base.tools,
174
+ ...(exec ? { image: exec.image } : {}),
175
+ ...(scope.pythonVersion === undefined ? {} : { python: scope.pythonVersion }),
176
+ ...(Object.keys(tools.versions()).length === 0 ? {} : { static: tools.versions() }),
177
+ },
178
+ ...(exec ? { execution: { feedbackLoop: feedbackLoop(exec), roots: executionRoots(exec) } } : {}),
179
+ ...(Object.keys(reportOnly).length === 0 ? {} : { reportOnly }),
180
+ ignoreSet: {
181
+ healthignoreSha256: scope.healthignoreSha256,
182
+ gitleaksignoreSha256: scope.gitleaksignoreSha256,
183
+ excludedLinesPercent: scope.excludedLinesPercent,
184
+ },
185
+ scope: {
186
+ logicFiles: scope.logicFiles,
187
+ logicLines: scope.logicLines,
188
+ sourceLines: scope.sourceLines,
189
+ packages: scope.packages,
190
+ },
191
+ parameters,
192
+ dimensions,
193
+ breakdown: breakdownFor(methodology, parameters),
194
+ health: healthFrom(methodology, dimensions),
195
+ };
196
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,42 @@
1
+ import { bandFor, dimensionScore, healthScore, roundHalfUp } from '../scoring/aggregate.js';
2
+ import { poolParameter } from '../scoring/pool.js';
3
+ function liveScore(def, param, units) {
4
+ if (!param)
5
+ return null;
6
+ if (param.status === 'failed')
7
+ return 0;
8
+ if (param.status !== 'measured')
9
+ return null;
10
+ return poolParameter(def, units ?? param.units ?? []).score;
11
+ }
12
+ export function dimensionScores(methodology, parameters, filter) {
13
+ return methodology.dimensions.map((dimension) => {
14
+ const weighted = dimension.parameters.map((def) => {
15
+ const param = parameters.find((p) => p.id === def.id);
16
+ if (filter && param?.status === 'measured') {
17
+ const units = (param.units ?? []).filter(filter);
18
+ // A package or language with no units for a parameter has no data for it.
19
+ return { weight: def.weight, score: units.length === 0 ? null : liveScore(def, param, units) };
20
+ }
21
+ return { weight: def.weight, score: liveScore(def, param) };
22
+ });
23
+ return { id: dimension.id, key: dimension.key, score: dimensionScore(weighted) };
24
+ });
25
+ }
26
+ export function healthFrom(methodology, dimensions) {
27
+ const score = healthScore(dimensions.map((d) => d.score), methodology.capMargin);
28
+ if (score === null)
29
+ return null;
30
+ const displayed = roundHalfUp(score);
31
+ return { score, displayed, band: bandFor(displayed, methodology.bands).key };
32
+ }
33
+ export function breakdownFor(methodology, parameters) {
34
+ const units = parameters.flatMap((p) => p.units ?? []);
35
+ const packages = [...new Set(units.map((u) => u.package))].sort();
36
+ const groups = [...new Set(units.map((u) => u.group))].sort();
37
+ const toRecord = (dims) => Object.fromEntries(dims.map((d) => [String(d.id), d.score]));
38
+ return {
39
+ byPackage: Object.fromEntries(packages.map((pkg) => [pkg, toRecord(dimensionScores(methodology, parameters, (u) => u.package === pkg))])),
40
+ byGroup: Object.fromEntries(groups.map((group) => [group, toRecord(dimensionScores(methodology, parameters, (u) => u.group === group))])),
41
+ };
42
+ }
@@ -0,0 +1,4 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ export function writeRun(file, run) {
3
+ writeFileSync(file, `${JSON.stringify(run, null, 2)}\n`, 'utf8');
4
+ }