@ultimat3/cli 22.15.0 → 24.0.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 (267) hide show
  1. package/CLAUDE.md +36 -13
  2. package/README.md +31 -8
  3. package/package.json +29 -28
  4. package/src/admin-catalog-keys.ts +16 -0
  5. package/src/admin-registration.ts +192 -0
  6. package/src/affected.ts +27 -35
  7. package/src/api-registration.ts +125 -11
  8. package/src/app-artifacts.ts +1 -1
  9. package/src/app-auth.ts +4 -27
  10. package/src/app-boundaries.ts +6 -20
  11. package/src/app-config-load.ts +51 -0
  12. package/src/app-env.ts +5 -11
  13. package/src/app-load.ts +55 -11
  14. package/src/app-manifest.ts +16 -8
  15. package/src/app-mcp.ts +10 -23
  16. package/src/app-permissions-borrowed.ts +129 -0
  17. package/src/app-permissions-site.ts +24 -0
  18. package/src/app-permissions.ts +0 -0
  19. package/src/app-reload-graph.ts +7 -30
  20. package/src/app-transport.ts +219 -0
  21. package/src/boundary-findings.ts +16 -4
  22. package/src/browser-transport.ts +160 -0
  23. package/src/budgets.ts +27 -34
  24. package/src/cdp-shot-driver.ts +3 -1
  25. package/src/cmd-auth-spec.ts +16 -0
  26. package/src/cmd-auth.ts +65 -0
  27. package/src/cmd-build-spec.ts +10 -3
  28. package/src/cmd-build.ts +107 -11
  29. package/src/cmd-db-backfill.ts +25 -12
  30. package/src/cmd-db.ts +66 -16
  31. package/src/cmd-deploy-helm.ts +7 -6
  32. package/src/cmd-deploy.ts +4 -4
  33. package/src/cmd-dev.ts +9 -8
  34. package/src/cmd-doctor-spec.ts +2 -9
  35. package/src/cmd-doctor.ts +75 -24
  36. package/src/cmd-env.ts +26 -2
  37. package/src/cmd-generate.ts +195 -47
  38. package/src/cmd-help.ts +19 -6
  39. package/src/cmd-i18n.ts +10 -9
  40. package/src/cmd-jobs-spec.ts +19 -3
  41. package/src/cmd-jobs.ts +136 -7
  42. package/src/cmd-manifest-spec.ts +1 -1
  43. package/src/cmd-manifest.ts +6 -25
  44. package/src/cmd-mcp.ts +46 -15
  45. package/src/cmd-new.ts +15 -1
  46. package/src/cmd-planned.ts +15 -8
  47. package/src/cmd-routes.ts +27 -3
  48. package/src/cmd-secrets-ring.ts +76 -0
  49. package/src/cmd-secrets-spec.ts +10 -2
  50. package/src/cmd-secrets.ts +95 -18
  51. package/src/cmd-shot-matrix.ts +1 -3
  52. package/src/cmd-shot.ts +12 -3
  53. package/src/cmd-tasks.ts +84 -15
  54. package/src/cmd-test-spec.ts +1 -7
  55. package/src/cmd-test.ts +13 -29
  56. package/src/cmd-verify.ts +13 -5
  57. package/src/comment-safe.ts +16 -0
  58. package/src/coverage-floor.ts +334 -0
  59. package/src/coverage-lcov.ts +193 -0
  60. package/src/coverage-source.ts +136 -0
  61. package/src/db-object-drift.ts +30 -0
  62. package/src/db-replay-engine.ts +115 -0
  63. package/src/db-schema-dump.ts +150 -0
  64. package/src/db-schema-refresh.ts +122 -0
  65. package/src/db-subscribes.ts +12 -4
  66. package/src/dev-dashboard-guard.ts +76 -0
  67. package/src/dev-dashboard.ts +51 -29
  68. package/src/dev-lock.ts +4 -7
  69. package/src/dev-route-table.ts +9 -0
  70. package/src/dispatch.ts +3 -6
  71. package/src/doctor-app-url.ts +39 -0
  72. package/src/doctor-auth.ts +95 -0
  73. package/src/doctor-coverage.ts +39 -0
  74. package/src/doctor-guards.ts +54 -0
  75. package/src/doctor-sealed.ts +132 -0
  76. package/src/document-graph.ts +82 -0
  77. package/src/drift-replica-identity.ts +42 -0
  78. package/src/error-catalog.ts +6 -9
  79. package/src/error-codes.ts +55 -3
  80. package/src/error-unthrown.ts +29 -11
  81. package/src/errors.ts +54 -83
  82. package/src/framework-schema-apply.ts +78 -0
  83. package/src/framework-schema-stamp.ts +96 -0
  84. package/src/framework-schema.ts +28 -2
  85. package/src/generate-files.ts +79 -12
  86. package/src/generate-grant-findings.ts +76 -0
  87. package/src/generate-grants.ts +77 -3
  88. package/src/generate-kinds.ts +52 -1
  89. package/src/generate-shadow.ts +46 -0
  90. package/src/generate-write.ts +62 -40
  91. package/src/generated-imports.ts +59 -0
  92. package/src/gh-target.ts +12 -7
  93. package/src/guard-sources.ts +104 -0
  94. package/src/guards.ts +23 -7
  95. package/src/handle-registration.ts +217 -0
  96. package/src/i18n-audit.ts +4 -1
  97. package/src/i18n-registration.ts +36 -0
  98. package/src/image-prepare.ts +51 -8
  99. package/src/import-closure.ts +165 -0
  100. package/src/index.ts +12 -5
  101. package/src/island-bundle.ts +3 -15
  102. package/src/island-store.ts +24 -16
  103. package/src/jobs-drain.ts +95 -21
  104. package/src/jobs-json.ts +50 -0
  105. package/src/jobs-report.ts +7 -4
  106. package/src/json-merge.ts +46 -0
  107. package/src/loopback-host.ts +13 -0
  108. package/src/manifest-staleness.ts +62 -0
  109. package/src/mcp-errors.ts +41 -4
  110. package/src/mcp-host.ts +16 -9
  111. package/src/mcp-test-run.ts +38 -0
  112. package/src/messages.ts +35 -0
  113. package/src/metrics-endpoint.ts +95 -24
  114. package/src/migration-extensions.ts +29 -0
  115. package/src/module-imports.ts +47 -0
  116. package/src/output.ts +24 -9
  117. package/src/page-navigation.ts +7 -23
  118. package/src/page-speculation.ts +5 -20
  119. package/src/permission-grants.ts +41 -9
  120. package/src/prerender-out.ts +34 -7
  121. package/src/prerender.ts +8 -5
  122. package/src/publish-closure.ts +101 -0
  123. package/src/pwa-artifacts.ts +26 -94
  124. package/src/registry.ts +2 -0
  125. package/src/revalidated-response.ts +4 -3
  126. package/src/role-load.ts +143 -0
  127. package/src/role-replicator.ts +16 -0
  128. package/src/role-start.ts +27 -5
  129. package/src/role-sync.ts +55 -20
  130. package/src/role-wake.ts +45 -0
  131. package/src/runtime-admin.ts +141 -0
  132. package/src/runtime-assets.ts +11 -7
  133. package/src/runtime-bindings.ts +4 -4
  134. package/src/runtime-cache.ts +38 -47
  135. package/src/runtime-hooks.ts +5 -2
  136. package/src/runtime-idempotency-scope.ts +23 -0
  137. package/src/runtime-isr.ts +36 -0
  138. package/src/runtime-jobs.ts +8 -33
  139. package/src/runtime-mfa-warning.ts +29 -0
  140. package/src/runtime-notify-retention.ts +10 -46
  141. package/src/runtime-overrides.ts +1 -1
  142. package/src/runtime-purge.ts +38 -16
  143. package/src/runtime-queue.ts +39 -11
  144. package/src/runtime-realtime.ts +9 -45
  145. package/src/runtime-render.ts +68 -35
  146. package/src/runtime-services.ts +22 -18
  147. package/src/runtime-storage.ts +10 -5
  148. package/src/scaffold-typecheck.ts +1 -0
  149. package/src/schema-diff.ts +16 -0
  150. package/src/schema-drift.ts +45 -15
  151. package/src/schema-dump-drift.ts +64 -0
  152. package/src/seo-routes.ts +80 -9
  153. package/src/serve-boot.ts +114 -158
  154. package/src/serve-drain.ts +9 -33
  155. package/src/serve-env.ts +9 -6
  156. package/src/serve-prebuilt-paths.ts +19 -0
  157. package/src/serve-prebuilt.ts +84 -0
  158. package/src/serve-web.ts +169 -0
  159. package/src/serve.ts +6 -9
  160. package/src/server-barrels.ts +54 -0
  161. package/src/shot-locale.ts +5 -23
  162. package/src/site-assets.ts +3 -5
  163. package/src/site-config.ts +9 -48
  164. package/src/site-seo.ts +1 -1
  165. package/src/sitemap-lastmod.ts +5 -0
  166. package/src/solid-loader.ts +12 -3
  167. package/src/source-edit.ts +49 -0
  168. package/src/source-files.ts +6 -0
  169. package/src/stored-object-headers.ts +43 -0
  170. package/src/sync-url.ts +2 -2
  171. package/src/tasks-facts.ts +3 -26
  172. package/src/templates/action.ts +147 -11
  173. package/src/templates/admin-catalog.ts +58 -0
  174. package/src/templates/admin-page.ts +36 -8
  175. package/src/templates/admin.ts +57 -17
  176. package/src/templates/backfill.ts +121 -87
  177. package/src/templates/entity.ts +140 -44
  178. package/src/templates/guard-animated-layout-property.ts +4 -11
  179. package/src/templates/guard-bare-error.ts +9 -13
  180. package/src/templates/guard-focus-visible.ts +4 -11
  181. package/src/templates/guard-image-dimensions.ts +6 -6
  182. package/src/templates/guard-island-without-states.ts +2 -4
  183. package/src/templates/guard-raw-breakpoint.ts +210 -0
  184. package/src/templates/guard-raw-colour.ts +4 -11
  185. package/src/templates/guard-raw-length.ts +375 -0
  186. package/src/templates/guard-raw-motion.ts +213 -0
  187. package/src/templates/guard-raw-shadow.ts +187 -0
  188. package/src/templates/guard-raw-z-index.ts +166 -0
  189. package/src/templates/guard-repo-raw-sql.ts +345 -0
  190. package/src/templates/guard-semantic-interactive.ts +6 -6
  191. package/src/templates/guard-undeclared-custom-property.ts +217 -0
  192. package/src/templates/guard-undefined-style-class.ts +215 -0
  193. package/src/templates/guard-untranslated-string.ts +6 -6
  194. package/src/templates/guard-unzoned-date.ts +9 -13
  195. package/src/templates/index.ts +3 -2
  196. package/src/templates/island.ts +34 -51
  197. package/src/templates/job-body-test.ts +121 -0
  198. package/src/templates/job.ts +23 -8
  199. package/src/templates/naming.ts +28 -0
  200. package/src/templates/policy.ts +16 -9
  201. package/src/templates/query.ts +242 -26
  202. package/src/templates/resource-create.ts +62 -18
  203. package/src/templates/resource-form-island.ts +58 -80
  204. package/src/templates/resource-page.ts +269 -0
  205. package/src/templates/resource-service.ts +96 -0
  206. package/src/templates/resource.ts +86 -61
  207. package/src/templates/route.ts +41 -35
  208. package/src/templates/scaffold-api.ts +55 -1
  209. package/src/templates/scaffold-app.ts +177 -53
  210. package/src/templates/scaffold-auth.ts +83 -19
  211. package/src/templates/scaffold-browser-client.ts +90 -0
  212. package/src/templates/scaffold-container-compose.ts +10 -1
  213. package/src/templates/scaffold-container.ts +20 -2
  214. package/src/templates/scaffold-dashboard-bare.ts +64 -2
  215. package/src/templates/scaffold-dashboard-example.ts +124 -31
  216. package/src/templates/scaffold-dashboard-shared.ts +1 -20
  217. package/src/templates/scaffold-dashboard.ts +1 -2
  218. package/src/templates/scaffold-db-client.ts +144 -0
  219. package/src/templates/scaffold-db-package.ts +90 -8
  220. package/src/templates/scaffold-docs.ts +50 -5
  221. package/src/templates/scaffold-guards.ts +54 -11
  222. package/src/templates/scaffold-helm-fences.ts +76 -0
  223. package/src/templates/scaffold-helm-templates.ts +32 -7
  224. package/src/templates/scaffold-helm.ts +31 -1
  225. package/src/templates/scaffold-http.ts +9 -7
  226. package/src/templates/scaffold-repo.ts +61 -3
  227. package/src/templates/scaffold-roles.ts +56 -9
  228. package/src/templates/scaffold-shell.ts +74 -105
  229. package/src/templates/scaffold-site.ts +45 -19
  230. package/src/templates/scaffold-ui-package.ts +18 -0
  231. package/src/templates/slice-foundation.ts +46 -8
  232. package/src/test-counts.ts +14 -2
  233. package/src/test-ignores.ts +25 -0
  234. package/src/test-passes.ts +0 -15
  235. package/src/test-select.ts +5 -14
  236. package/src/test-shards.ts +41 -93
  237. package/src/test-workers.ts +26 -4
  238. package/src/theme-boot.ts +6 -31
  239. package/src/transport-calls.ts +105 -0
  240. package/src/trusted-hops-error.ts +17 -0
  241. package/src/unscanned-admin.ts +54 -0
  242. package/src/unthrown-codes.ts +26 -0
  243. package/src/verify-checks.ts +43 -53
  244. package/src/verify-coverage-run.ts +190 -0
  245. package/src/verify-deadline.ts +353 -0
  246. package/src/verify-floor.ts +36 -9
  247. package/src/verify-merge.ts +171 -45
  248. package/src/verify-part-step.ts +90 -0
  249. package/src/verify-progress.ts +25 -0
  250. package/src/verify-role-load-probe.ts +54 -0
  251. package/src/verify-role-load.ts +148 -0
  252. package/src/verify-run.ts +116 -37
  253. package/src/verify-shard.ts +33 -11
  254. package/src/verify-stalled.ts +152 -0
  255. package/src/verify-step.ts +42 -1
  256. package/src/verify-tests.ts +83 -8
  257. package/src/workspace-checks.ts +50 -8
  258. package/src/workspace-dep-edit.ts +127 -0
  259. package/src/build-errors.ts +0 -54
  260. package/src/cmd-dev-fixture.ts +0 -204
  261. package/src/dev-live-fixture.ts +0 -122
  262. package/src/policy-fixture.ts +0 -78
  263. package/src/realtime-browser-probe-fixture.ts +0 -9
  264. package/src/role-start-fixture.ts +0 -78
  265. package/src/scaffold-fixture.ts +0 -160
  266. package/src/templates/island-fixture.ts +0 -76
  267. package/src/verify-errors.ts +0 -49
@@ -0,0 +1,334 @@
1
+ // The coverage floor an app states in `x.verify.json`, what the unit suite measured against the
2
+ // app's WHOLE source tree, and the findings where the two disagree. A finding of the `unit` step,
3
+ // never a step of its own: the step list is as stable as an error code.
4
+
5
+ // why: Bun ships no synchronous existence check and no path-join primitive; the fix a
6
+ // below-floor finding carries depends on whether the test beside the worst file exists.
7
+ import { existsSync } from 'node:fs';
8
+ // why: Bun ships no path-join primitive.
9
+ import { join } from 'node:path';
10
+ import { ERROR_DOCS_URL } from '@ultimat3/core';
11
+ import type { CoverageMap } from './coverage-lcov';
12
+ import { percent } from './coverage-lcov';
13
+ import { appSourceFiles, hasExecutableCode, readSource, unloadedWeight } from './coverage-source';
14
+ import { msg } from './messages';
15
+ import type { Finding } from './output';
16
+ import { quoteArg } from './shell-quote';
17
+
18
+ /** The `x.verify.json` key. */
19
+ export const COVERAGE_FIELD = 'coverage';
20
+
21
+ /**
22
+ * Lines and functions, the framework's own code and every app's — one bar. **A floor, not the
23
+ * goal**: coverage measures execution, not validation, and a test added to raise it is proven by
24
+ * mutation (break the source, watch it go red, restore). A covered branch whose test cannot fail
25
+ * is worse than an uncovered one.
26
+ */
27
+ export const COVERAGE_BAR = 95;
28
+
29
+ /** How far above a floor under the bar the tree may measure before the floor has to rise. */
30
+ export const FLOOR_SLACK = 1.5;
31
+
32
+ /** How many files a below-floor finding names. */
33
+ export const LOSS_LIMIT = 10;
34
+
35
+ const FLOOR_FILE = 'x.verify.json';
36
+
37
+ export interface CoverageExclude {
38
+ /** Relative to the app root. */
39
+ readonly glob: string;
40
+ /** Why a unit test cannot execute it — a browser-only mount, a container entry point. */
41
+ readonly why: string;
42
+ }
43
+
44
+ export interface CoverageFloor {
45
+ readonly lines: number;
46
+ readonly funcs: number;
47
+ /** Required while either number is under `COVERAGE_BAR`: what is uncovered, who closes it. */
48
+ readonly why?: string;
49
+ readonly exclude: readonly CoverageExclude[];
50
+ }
51
+
52
+ const isPercent = (value: unknown): value is number =>
53
+ typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 100;
54
+
55
+ const isText = (value: unknown): value is string =>
56
+ typeof value === 'string' && value.trim().length > 0;
57
+
58
+ /**
59
+ * `coverage` out of `x.verify.json`, or the reasons it is not a floor. A number that is not a
60
+ * percentage is refused rather than read as zero — `measured < NaN` is false for every reading,
61
+ * so a typo would stop the floor enforcing and report green. An `exclude` entry with no `why` is
62
+ * refused too, and DROPPED: it excludes nothing until it says what it is for.
63
+ */
64
+ export function readCoverageFloor(payload: Record<string, unknown> | undefined): {
65
+ coverage?: CoverageFloor;
66
+ problems: readonly string[];
67
+ } {
68
+ const raw = payload?.[COVERAGE_FIELD];
69
+ if (raw === undefined) return { problems: [] };
70
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
71
+ return { problems: [`"${COVERAGE_FIELD}" is not an object of { "lines", "funcs" }`] };
72
+ }
73
+ const record = raw as Record<string, unknown>;
74
+ const problems: string[] = [];
75
+ for (const key of ['lines', 'funcs'] as const) {
76
+ if (!isPercent(record[key])) {
77
+ problems.push(
78
+ `"${COVERAGE_FIELD}.${key}" is ${JSON.stringify(record[key])}, which is not a percentage from 0 to 100`,
79
+ );
80
+ }
81
+ }
82
+ if (record['why'] !== undefined && !isText(record['why'])) {
83
+ problems.push(`"${COVERAGE_FIELD}.why" is not a sentence`);
84
+ }
85
+ const exclude: CoverageExclude[] = [];
86
+ const list = record['exclude'];
87
+ if (list !== undefined && !Array.isArray(list)) {
88
+ problems.push(`"${COVERAGE_FIELD}.exclude" is not a list of { "glob", "why" }`);
89
+ }
90
+ for (const entry of Array.isArray(list) ? (list as unknown[]) : []) {
91
+ const one =
92
+ typeof entry === 'object' && entry !== null ? (entry as Record<string, unknown>) : {};
93
+ if (!isText(one['glob'])) {
94
+ problems.push(`"${COVERAGE_FIELD}.exclude" holds an entry with no "glob"`);
95
+ } else if (!isText(one['why'])) {
96
+ problems.push(
97
+ `"${COVERAGE_FIELD}.exclude" excludes ${one['glob']} with no "why" — say why a unit test cannot execute it`,
98
+ );
99
+ } else exclude.push({ glob: one['glob'], why: one['why'] });
100
+ }
101
+ if (!isPercent(record['lines']) || !isPercent(record['funcs'])) return { problems };
102
+ return {
103
+ coverage: {
104
+ lines: record['lines'],
105
+ funcs: record['funcs'],
106
+ ...(isText(record['why']) ? { why: record['why'] } : {}),
107
+ exclude,
108
+ },
109
+ problems,
110
+ };
111
+ }
112
+
113
+ /** One source file's share of the gap. */
114
+ export interface FileLoss {
115
+ readonly file: string;
116
+ /** Lines that could have run and did not. */
117
+ readonly uncovered: number;
118
+ readonly lines: number;
119
+ /** No unit test loaded it at all: it counts at 0%, weighed off its source. */
120
+ readonly unloaded: boolean;
121
+ }
122
+
123
+ export interface CoverageMeasure {
124
+ readonly lines: number;
125
+ readonly funcs: number;
126
+ readonly linesHit: number;
127
+ readonly linesFound: number;
128
+ readonly funcsHit: number;
129
+ readonly funcsFound: number;
130
+ /** Source files counted, loaded or not. */
131
+ readonly files: number;
132
+ /** Worst first: most uncovered lines, then by path. */
133
+ readonly losses: readonly FileLoss[];
134
+ }
135
+
136
+ /**
137
+ * The suite's coverage of the app's WHOLE source tree. `coverage` is keyed by path relative to
138
+ * `root`; a record for anything else — a framework package, `node_modules`, a test — is not the
139
+ * app's source and is dropped. A source file with executable code and no record counts at 0%.
140
+ */
141
+ export async function measureCoverage(
142
+ root: string,
143
+ coverage: CoverageMap,
144
+ floor: CoverageFloor | undefined,
145
+ ): Promise<CoverageMeasure> {
146
+ const sources = appSourceFiles(root, floor?.exclude.map((entry) => entry.glob) ?? []);
147
+ let linesHit = 0;
148
+ let linesFound = 0;
149
+ let funcsHit = 0;
150
+ let funcsFound = 0;
151
+ let files = 0;
152
+ const losses: FileLoss[] = [];
153
+ for (const file of sources) {
154
+ const loaded = Object.hasOwn(coverage, file) ? coverage[file] : undefined;
155
+ if (loaded !== undefined) {
156
+ const found = loaded.hit.length + loaded.miss.length;
157
+ linesHit += loaded.hit.length;
158
+ linesFound += found;
159
+ funcsHit += Math.min(loaded.funcsHit, loaded.funcsFound);
160
+ funcsFound += loaded.funcsFound;
161
+ files += 1;
162
+ if (loaded.miss.length > 0) {
163
+ losses.push({ file, uncovered: loaded.miss.length, lines: found, unloaded: false });
164
+ }
165
+ continue;
166
+ }
167
+ const source = await readSource(root, file);
168
+ // A barrel or a types-only module emits nothing, so Bun writes no record for it when it IS
169
+ // loaded: absent is correct, and it weighs nothing.
170
+ if (source === undefined || !hasExecutableCode(source)) continue;
171
+ const weight = unloadedWeight(source);
172
+ linesFound += weight.lines;
173
+ funcsFound += weight.funcs;
174
+ files += 1;
175
+ losses.push({ file, uncovered: weight.lines, lines: weight.lines, unloaded: true });
176
+ }
177
+ losses.sort((a, b) => b.uncovered - a.uncovered || (a.file < b.file ? -1 : 1));
178
+ return {
179
+ lines: percent(linesHit, linesFound),
180
+ funcs: percent(funcsHit, funcsFound),
181
+ linesHit,
182
+ linesFound,
183
+ funcsHit,
184
+ funcsFound,
185
+ files,
186
+ losses,
187
+ };
188
+ }
189
+
190
+ /** One decimal, rounded DOWN: a printed number never claims more than was measured. */
191
+ const shown = (value: number): string => (Math.floor(value * 10) / 10).toFixed(1);
192
+
193
+ /**
194
+ * The floor a measured number supports: the bar once it clears it, the whole number under it
195
+ * below — up to a point of room, so one uncovered line in an unrelated file is not a red gate,
196
+ * and less than `FLOOR_SLACK`, so the floor it writes is never stale the moment it is written.
197
+ */
198
+ const floorFor = (measured: number): number =>
199
+ measured >= COVERAGE_BAR ? COVERAGE_BAR : Math.floor(measured);
200
+
201
+ const WHY_PLACEHOLDER = '<what is uncovered and who closes it>';
202
+
203
+ /** The `"coverage": { … }` text for a measured tree — what an author pastes. */
204
+ export function floorLine(measure: Pick<CoverageMeasure, 'lines' | 'funcs'>): string {
205
+ const lines = floorFor(measure.lines);
206
+ const funcs = floorFor(measure.funcs);
207
+ const why = lines < COVERAGE_BAR || funcs < COVERAGE_BAR ? `, "why": "${WHY_PLACEHOLDER}"` : '';
208
+ return `"${COVERAGE_FIELD}": { "lines": ${String(lines)}, "funcs": ${String(funcs)}${why} }`;
209
+ }
210
+
211
+ /**
212
+ * `foo.ts` → `foo.test.ts`, and `page.tsx` → `page.test.ts`: where the test that covers a file
213
+ * lives. Always `.ts` — every generator writes it so, and a test holds no JSX: the registry leak
214
+ * guard's loader covers `.test.ts` only, so a `.test.tsx` this named would also be unguarded.
215
+ */
216
+ export const testPathOf = (file: string): string => file.replace(/\.tsx?$/, '.test.ts');
217
+
218
+ export interface JudgeOptions {
219
+ /** Whether `path` (relative to the app root) exists — decides which fix a below-floor gets. */
220
+ readonly exists: (path: string) => boolean;
221
+ }
222
+
223
+ /**
224
+ * The findings a measurement earns against the stated floor — none is the only pass.
225
+ *
226
+ * | State | Finding |
227
+ * |---|---|
228
+ * | no floor stated | `X_COVERAGE_FLOOR_UNSTATED`, carrying the line to add |
229
+ * | a floor under the bar with no `why` | `X_COVERAGE_FLOOR_UNSTATED` |
230
+ * | measured under the floor | `X_COVERAGE_BELOW_FLOOR`, naming the files that lose the most |
231
+ * | a floor under the bar, and the tree `FLOOR_SLACK` points over it | `X_COVERAGE_FLOOR_STALE` |
232
+ *
233
+ * The last row is what makes the floor only rise, and it needs no history to do it: a floor may
234
+ * sit under the bar only while it is what the tree measures.
235
+ */
236
+ export function coverageFindings(
237
+ measure: CoverageMeasure,
238
+ floor: CoverageFloor | undefined,
239
+ options: JudgeOptions,
240
+ ): readonly Finding[] {
241
+ const measured = `${shown(measure.lines)}% of lines and ${shown(measure.funcs)}% of functions`;
242
+ if (floor === undefined) {
243
+ return [
244
+ {
245
+ code: 'X_COVERAGE_FLOOR_UNSTATED',
246
+ cause: `${FLOOR_FILE} states no coverage floor; the unit suite covers ${measured} of this app's own source (${String(measure.files)} files, one no test loads counted at 0%)`,
247
+ fix: `edit ${FLOOR_FILE} — add ${floorLine(measure)}`,
248
+ docs: ERROR_DOCS_URL,
249
+ at: FLOOR_FILE,
250
+ },
251
+ ];
252
+ }
253
+ const findings: Finding[] = [];
254
+ const stated = `${shown(floor.lines)}% / ${shown(floor.funcs)}%`;
255
+ const underBar = floor.lines < COVERAGE_BAR || floor.funcs < COVERAGE_BAR;
256
+ if (underBar && floor.why === undefined) {
257
+ findings.push({
258
+ code: 'X_COVERAGE_FLOOR_UNSTATED',
259
+ cause: `${FLOOR_FILE} states a coverage floor of ${stated}, under the ${String(COVERAGE_BAR)}% bar, and no reason for it`,
260
+ fix: `edit ${FLOOR_FILE} — add "why": "${WHY_PLACEHOLDER}" inside "${COVERAGE_FIELD}", or raise both numbers to ${String(COVERAGE_BAR)}`,
261
+ docs: ERROR_DOCS_URL,
262
+ at: FLOOR_FILE,
263
+ });
264
+ }
265
+ if (measure.lines < floor.lines || measure.funcs < floor.funcs) {
266
+ const worst = measure.losses.slice(0, LOSS_LIMIT);
267
+ const named = worst
268
+ .map((loss) =>
269
+ loss.unloaded
270
+ ? `${loss.file} (no unit test loads it: ${String(loss.lines)} lines)`
271
+ : `${loss.file} (${String(loss.uncovered)} of ${String(loss.lines)} lines)`,
272
+ )
273
+ .join(', ');
274
+ const first = worst[0];
275
+ const test = first === undefined ? undefined : testPathOf(first.file);
276
+ findings.push({
277
+ code: 'X_COVERAGE_BELOW_FLOOR',
278
+ cause: `the unit suite covers ${measured} of this app's own source, under the floor of ${stated} in ${FLOOR_FILE}${named === '' ? '' : ` — losing the most lines: ${named}`}`,
279
+ fix:
280
+ test === undefined
281
+ ? 'x verify --only unit --json'
282
+ : options.exists(test)
283
+ ? // Quoted: a route directory is `[id]`, which a shell reads as a glob.
284
+ `bun test --coverage ${quoteArg(test)}`
285
+ : `edit ${test} — no test sits beside ${first?.file ?? test}; write one that fails when the source is broken, then: bun test --coverage ${quoteArg(test)}`,
286
+ docs: ERROR_DOCS_URL,
287
+ ...(first === undefined ? {} : { at: first.file }),
288
+ });
289
+ }
290
+ const risen = (['lines', 'funcs'] as const).filter(
291
+ (key) => floor[key] < COVERAGE_BAR && measure[key] >= floor[key] + FLOOR_SLACK,
292
+ );
293
+ if (risen.length > 0) {
294
+ const lines = Math.max(floor.lines, floorFor(measure.lines));
295
+ const funcs = Math.max(floor.funcs, floorFor(measure.funcs));
296
+ const cleared = lines >= COVERAGE_BAR && funcs >= COVERAGE_BAR;
297
+ findings.push({
298
+ code: 'X_COVERAGE_FLOOR_STALE',
299
+ cause: `the unit suite now covers ${measured}, more than ${String(FLOOR_SLACK)} points over the floor of ${stated} in ${FLOOR_FILE} — the floor only rises`,
300
+ fix: `edit ${FLOOR_FILE} — set "lines": ${String(lines)} and "funcs": ${String(funcs)} inside "${COVERAGE_FIELD}"${cleared ? ', and delete its "why"' : ''}`,
301
+ docs: ERROR_DOCS_URL,
302
+ at: FLOOR_FILE,
303
+ });
304
+ }
305
+ return findings;
306
+ }
307
+
308
+ export interface CoverageVerdict {
309
+ readonly measure: CoverageMeasure;
310
+ readonly findings: readonly Finding[];
311
+ /** One line for the step's `output`: the numbers, and the floor they were held to. */
312
+ readonly output: string;
313
+ }
314
+
315
+ /** Measure `coverage` against the app at `root` and judge it against `floor` — the whole check. */
316
+ export async function judgeCoverage(
317
+ root: string,
318
+ coverage: CoverageMap,
319
+ floor: CoverageFloor | undefined,
320
+ ): Promise<CoverageVerdict> {
321
+ const measure = await measureCoverage(root, coverage, floor);
322
+ return {
323
+ measure,
324
+ findings: coverageFindings(measure, floor, {
325
+ exists: (path) => existsSync(join(root, path)),
326
+ }),
327
+ output: msg('cli.verify.coverage', {
328
+ lines: shown(measure.lines),
329
+ funcs: shown(measure.funcs),
330
+ files: measure.files,
331
+ floor: floor === undefined ? 'unstated' : `${shown(floor.lines)}% / ${shown(floor.funcs)}%`,
332
+ }),
333
+ };
334
+ }
@@ -0,0 +1,193 @@
1
+ // The one lcov reader: what `bun test --coverage --coverage-reporter=lcov` wrote, as per-file
2
+ // facts, and how the facts of several test processes add up. `scripts/coverage-gate.ts` (the
3
+ // framework's packages) and `coverage-floor.ts` (an app) both read through it.
4
+
5
+ /** One `SF:` record, as written: its own totals and every `DA:` line. */
6
+ export interface LcovRecord {
7
+ /** The `SF:` path, verbatim — Bun writes it relative to the directory the run started in. */
8
+ readonly file: string;
9
+ /** `LF` / `LH` / `FNF` / `FNH`. Bun's lcov carries no `FN:` lines, so functions are counts only. */
10
+ readonly linesFound: number;
11
+ readonly linesHit: number;
12
+ readonly funcsFound: number;
13
+ readonly funcsHit: number;
14
+ /** line → hits. */
15
+ readonly lines: ReadonlyMap<number, number>;
16
+ }
17
+
18
+ export function parseLcov(text: string): readonly LcovRecord[] {
19
+ const records: LcovRecord[] = [];
20
+ let file: string | undefined;
21
+ let lf = 0;
22
+ let lh = 0;
23
+ let fnf = 0;
24
+ let fnh = 0;
25
+ let lines = new Map<number, number>();
26
+ for (const line of text.split('\n')) {
27
+ if (line.startsWith('SF:')) {
28
+ file = line.slice(3);
29
+ lf = 0;
30
+ lh = 0;
31
+ fnf = 0;
32
+ fnh = 0;
33
+ lines = new Map();
34
+ } else if (file === undefined) {
35
+ // Before the first record (`TN:`), or between two.
36
+ } else if (line.startsWith('DA:')) {
37
+ const comma = line.indexOf(',');
38
+ const at = Number(line.slice(3, comma));
39
+ const hits = Number(line.slice(comma + 1));
40
+ if (comma > 3 && Number.isSafeInteger(at) && Number.isFinite(hits)) lines.set(at, hits);
41
+ } else if (line.startsWith('LF:')) lf = Number(line.slice(3));
42
+ else if (line.startsWith('LH:')) lh = Number(line.slice(3));
43
+ else if (line.startsWith('FNF:')) fnf = Number(line.slice(4));
44
+ else if (line.startsWith('FNH:')) fnh = Number(line.slice(4));
45
+ else if (line === 'end_of_record') {
46
+ records.push({
47
+ file,
48
+ linesFound: lf,
49
+ linesHit: lh,
50
+ funcsFound: fnf,
51
+ funcsHit: fnh,
52
+ lines,
53
+ });
54
+ file = undefined;
55
+ }
56
+ }
57
+ return records;
58
+ }
59
+
60
+ /** One source file's coverage: which lines ran, which could have and did not, and its functions. */
61
+ export interface FileCoverage {
62
+ readonly hit: readonly number[];
63
+ readonly miss: readonly number[];
64
+ readonly funcsFound: number;
65
+ readonly funcsHit: number;
66
+ }
67
+
68
+ export const fileCoverageOf = (record: LcovRecord): FileCoverage => {
69
+ const hit: number[] = [];
70
+ const miss: number[] = [];
71
+ for (const [line, hits] of record.lines) (hits > 0 ? hit : miss).push(line);
72
+ return { hit, miss, funcsFound: record.funcsFound, funcsHit: record.funcsHit };
73
+ };
74
+
75
+ /**
76
+ * One file's coverage across several test PROCESSES.
77
+ *
78
+ * A line is covered when any process ran it. It is uncovered only when EVERY process that loaded
79
+ * the file lists it as unrun — because Bun lists a function nothing called in that process by its
80
+ * whole line range, comments and blank lines included, and lists a function that ran by its
81
+ * executable lines alone. A union therefore counts the comments of every function one process
82
+ * called and another merely loaded, and the number moves with how the files were dealt:
83
+ * `examples/dummy` read 59.9% at four workers, 65.0% at two and 65.5% in one process, same tree
84
+ * (measured 2026-10-01, Bun 1.4.0, whose own `--parallel` merge is that union). This rule read
85
+ * 65.90% for 2, 4 and 8 processes and for contiguous chunks — and is exactly what Bun reports for
86
+ * one `--isolate` process.
87
+ *
88
+ * Functions have no identity in Bun's lcov, only two counts, so they cannot be unioned: the file
89
+ * takes the best any one process reached. That is a lower bound, and the reason the processes are
90
+ * cut along directories (`verify-coverage-run.ts`) — the tests beside a file run together.
91
+ */
92
+ export function mergeFileCoverage(parts: readonly FileCoverage[]): FileCoverage {
93
+ if (parts.length === 1) return parts[0] as FileCoverage;
94
+ const hit = new Set<number>();
95
+ for (const part of parts) for (const line of part.hit) hit.add(line);
96
+ const missIn = parts.map((part) => new Set(part.miss));
97
+ const miss = [...(missIn[0] ?? [])].filter(
98
+ (line) => !hit.has(line) && missIn.every((set) => set.has(line)),
99
+ );
100
+ return {
101
+ hit: [...hit].sort((a, b) => a - b),
102
+ miss: miss.sort((a, b) => a - b),
103
+ funcsFound: Math.max(0, ...parts.map((part) => part.funcsFound)),
104
+ funcsHit: Math.max(0, ...parts.map((part) => part.funcsHit)),
105
+ };
106
+ }
107
+
108
+ /** file → coverage. A plain record so it survives `--json` between a shard and `x verify merge`. */
109
+ export type CoverageMap = Readonly<Record<string, FileCoverage>>;
110
+
111
+ /** Several processes' (or several shards') maps as one, file by file. */
112
+ export function mergeCoverage(maps: readonly CoverageMap[]): CoverageMap {
113
+ const byFile = new Map<string, FileCoverage[]>();
114
+ for (const map of maps) {
115
+ for (const [file, coverage] of Object.entries(map)) {
116
+ const list = byFile.get(file) ?? [];
117
+ list.push(coverage);
118
+ byFile.set(file, list);
119
+ }
120
+ }
121
+ return Object.fromEntries(
122
+ [...byFile]
123
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
124
+ .map(([file, parts]) => [file, mergeFileCoverage(parts)]),
125
+ );
126
+ }
127
+
128
+ /** `hit / found` as a percentage to two decimals; nothing found is 0, never a pass over nothing. */
129
+ export const percent = (hit: number, found: number): number =>
130
+ found === 0 ? 0 : Math.round((hit / found) * 10_000) / 100;
131
+
132
+ /** `[1,2,3,7]` → `"1-3,7"`: a shard's facts ride in its `--json` document, so they are kept small. */
133
+ export function encodeLines(lines: readonly number[]): string {
134
+ const sorted = [...lines].sort((a, b) => a - b);
135
+ const runs: string[] = [];
136
+ let start: number | undefined;
137
+ let last = 0;
138
+ const flush = (): void => {
139
+ if (start !== undefined) runs.push(start === last ? String(start) : `${start}-${last}`);
140
+ };
141
+ for (const line of sorted) {
142
+ if (start !== undefined && line === last + 1) {
143
+ last = line;
144
+ continue;
145
+ }
146
+ flush();
147
+ start = line;
148
+ last = line;
149
+ }
150
+ flush();
151
+ return runs.join(',');
152
+ }
153
+
154
+ /** The inverse of `encodeLines`; a run that does not parse is no lines, never a throw. */
155
+ export function decodeLines(text: string): readonly number[] {
156
+ const lines: number[] = [];
157
+ for (const run of text.split(',')) {
158
+ const match = /^(\d+)(?:-(\d+))?$/.exec(run);
159
+ if (match === null) continue;
160
+ const from = Number(match[1]);
161
+ const to = match[2] === undefined ? from : Number(match[2]);
162
+ for (let line = from; line <= to && line - from < 1_000_000; line += 1) lines.push(line);
163
+ }
164
+ return lines;
165
+ }
166
+
167
+ /** A file's facts as they cross `--json`: hit runs, miss runs, `[funcsFound, funcsHit]`. */
168
+ export type CoverageWire = Readonly<
169
+ Record<string, { readonly h: string; readonly m: string; readonly f: readonly [number, number] }>
170
+ >;
171
+
172
+ export const encodeCoverage = (map: CoverageMap): CoverageWire =>
173
+ Object.fromEntries(
174
+ Object.entries(map).map(([file, one]) => [
175
+ file,
176
+ { h: encodeLines(one.hit), m: encodeLines(one.miss), f: [one.funcsFound, one.funcsHit] },
177
+ ]),
178
+ );
179
+
180
+ /** A part's `data.coverage.<step>` back into a map, or nothing when it is not one. */
181
+ export function decodeCoverage(value: unknown): CoverageMap | undefined {
182
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined;
183
+ const out = new Map<string, FileCoverage>();
184
+ for (const [file, raw] of Object.entries(value)) {
185
+ if (typeof raw !== 'object' || raw === null) return undefined;
186
+ const { h, m, f } = raw as { h?: unknown; m?: unknown; f?: unknown };
187
+ if (typeof h !== 'string' || typeof m !== 'string' || !Array.isArray(f)) return undefined;
188
+ const [found, hit] = f as unknown[];
189
+ if (typeof found !== 'number' || typeof hit !== 'number') return undefined;
190
+ out.set(file, { hit: decodeLines(h), miss: decodeLines(m), funcsFound: found, funcsHit: hit });
191
+ }
192
+ return Object.fromEntries(out);
193
+ }
@@ -0,0 +1,136 @@
1
+ // What counts as an app's OWN source for the coverage floor, and what a file no test loaded
2
+ // weighs. Bun writes an lcov record only for a file something imported, so a module nothing
3
+ // reaches is absent from both halves of the fraction — and makes the number BETTER.
4
+
5
+ // why: Bun ships no path-joining primitive; `join` builds the host path to each source file.
6
+ import { join } from 'node:path';
7
+ import { maskLiterals, stripComments } from '@ultimat3/core';
8
+
9
+ /** A test file, whatever its suite: `*.test.ts`, `*.contract.test.tsx`, … */
10
+ export const TEST_FILE = /\.test\.[cm]?[jt]sx?$/;
11
+
12
+ /** Index just past the balanced `{ … }` that starts at or after `from`. */
13
+ function skipBalanced(text: string, from: number): number {
14
+ let depth = 0;
15
+ for (let i = from; i < text.length; i += 1) {
16
+ const ch = text[i];
17
+ if (ch === '{') depth += 1;
18
+ else if (ch === '}') {
19
+ depth -= 1;
20
+ if (depth === 0) return i + 1;
21
+ }
22
+ }
23
+ return text.length;
24
+ }
25
+
26
+ /**
27
+ * Index just past the `;` that ends a `type` alias — the first one at nesting depth zero.
28
+ *
29
+ * Depth matters and a naive scan gets it wrong: `type _A = Assert<[FactKeysOf<{ a: 1 }>] extends
30
+ * [Actor] ? true : false>;` contains a `{ … }` whose closing brace is NOT the end of the
31
+ * declaration, and matching braces there leaves `, ] extends [Actor] … >;` behind, which then
32
+ * reads as executable code.
33
+ */
34
+ function endOfTypeAlias(text: string, from: number): number {
35
+ let depth = 0;
36
+ for (let i = from; i < text.length; i += 1) {
37
+ const ch = text[i];
38
+ if (ch === '{' || ch === '[' || ch === '(' || ch === '<') depth += 1;
39
+ else if (ch === '}' || ch === ']' || ch === ')' || ch === '>') depth -= 1;
40
+ else if (ch === ';' && depth <= 0) return i + 1;
41
+ }
42
+ return text.length;
43
+ }
44
+
45
+ /**
46
+ * Whether a file emits anything at RUNTIME, as opposed to a barrel or a types-only module.
47
+ *
48
+ * Load-bearing, not cosmetic: bun writes an lcov record only for a file that produced executable
49
+ * code, so a pure barrel and a types-only module are legitimately absent — while a real module
50
+ * nothing imported is absent for the opposite reason and must be reported.
51
+ * `packages/core/src/type-pins.ts` says it of itself: "This module emits nothing and exports
52
+ * nothing anybody imports."
53
+ *
54
+ * A regex cannot decide this. `metrics-types.ts` contains `(() => number)` in a TYPE position, so
55
+ * scanning for `=>` calls a types-only file executable. Hence a scanner: strip comments and
56
+ * re-export statements, then remove `interface` blocks by brace matching and `type` aliases to
57
+ * their depth-zero `;`, and ask whether anything is left.
58
+ *
59
+ * `declare module` and `declare global` are removed the same way, and for the same reason the
60
+ * `declare const` line exists: an ambient block emits nothing at runtime, so bun writes no lcov
61
+ * record for a file built only from one. Their bodies go with them — an augmentation's members
62
+ * are declarations whatever they look like. Without this, `matcher-surface.ts` (a
63
+ * `declare module 'bun:test'` and nothing else) read as executable and was reported as a real
64
+ * module no test imports, which is the opposite of what it is.
65
+ */
66
+ export function hasExecutableCode(source: string): boolean {
67
+ let text = source
68
+ .replace(/\/\*[\s\S]*?\*\//g, '')
69
+ .replace(/^[ \t]*\/\/.*$/gm, '')
70
+ .replace(/^[ \t]*(?:import|export)\b[^;]*?from\s*'[^']*';[ \t]*$/gm, '')
71
+ .replace(/^[ \t]*import\s+type\b[^;]*;[ \t]*$/gm, '')
72
+ // `declare const x: T;` is ambient — it emits nothing, and `type-pins.ts` files are built
73
+ // almost entirely from them.
74
+ .replace(/^[ \t]*declare\s+(?:const|let|var|function|class)\b[^;]*;[ \t]*$/gm, '');
75
+
76
+ // Ambient blocks first: their bodies hold `interface` members that the loop below would strip
77
+ // individually, leaving a bare `declare module '…' { }` shell behind that reads as code.
78
+ for (;;) {
79
+ const ambient = /(?:^|\n)[ \t]*declare\s+(?:module\s+'[^']*'|global)\s*\{/.exec(text);
80
+ if (ambient === null) break;
81
+ const start = ambient.index + (ambient[0].startsWith('\n') ? 1 : 0);
82
+ text = text.slice(0, start) + text.slice(skipBalanced(text, text.indexOf('{', start)));
83
+ }
84
+
85
+ for (;;) {
86
+ const found = /(?:^|\n)[ \t]*(?:export\s+)?(?:declare\s+)?(interface|type)\s/.exec(text);
87
+ if (found === null) break;
88
+ const start = found.index + (found[0].startsWith('\n') ? 1 : 0);
89
+ const end =
90
+ found[1] === 'interface'
91
+ ? skipBalanced(text, text.indexOf('{', start))
92
+ : endOfTypeAlias(text, start);
93
+ text = text.slice(0, start) + text.slice(end);
94
+ }
95
+ return text.replace(/\s/g, '') !== '';
96
+ }
97
+
98
+ /**
99
+ * What a file with NO lcov record weighs, read off its source because nothing else knows: its
100
+ * non-blank, non-comment lines, and one function per `function` keyword or arrow (at least one —
101
+ * it has executable code, or it would not be asked). An estimate, deliberately on the heavy side
102
+ * of what Bun reports for a loaded file (1.34x over `examples/dummy`'s loaded sources,
103
+ * 2026-10-01): the only way to make it exact is to load the file from a test, which is the fix.
104
+ */
105
+ export function unloadedWeight(source: string): { readonly lines: number; readonly funcs: number } {
106
+ const stripped = stripComments(source);
107
+ const lines = stripped.split('\n').filter((line) => line.trim() !== '').length;
108
+ const funcs = (maskLiterals(stripped).match(/\bfunction\b|=>/g) ?? []).length;
109
+ return { lines: Math.max(1, lines), funcs: Math.max(1, funcs) };
110
+ }
111
+
112
+ /** Never an app's own source, wherever it sits. */
113
+ const NOT_SOURCE = /(?:^|\/)(?:node_modules|dist|build|\.x)\//;
114
+
115
+ /**
116
+ * Every `.ts` / `.tsx` under the app's `apps/` and `packages/` that is its own source: not a
117
+ * test, not a declaration file, not installed or built output, and not excluded by `exclude`
118
+ * (globs relative to the app root). Sorted, so two runs of one tree read one list.
119
+ */
120
+ export function appSourceFiles(root: string, exclude: readonly string[] = []): readonly string[] {
121
+ const globs = exclude.map((pattern) => new Bun.Glob(pattern));
122
+ const files: string[] = [];
123
+ for (const rel of new Bun.Glob('{apps,packages}/**/*.{ts,tsx}').scanSync({ cwd: root })) {
124
+ const path = rel.split('\\').join('/');
125
+ if (NOT_SOURCE.test(path) || TEST_FILE.test(path) || path.endsWith('.d.ts')) continue;
126
+ if (globs.some((glob) => glob.match(path))) continue;
127
+ files.push(path);
128
+ }
129
+ return files.sort();
130
+ }
131
+
132
+ /** The source text of `rel` under `root`, or nothing when it cannot be read. */
133
+ export const readSource = (root: string, rel: string): Promise<string | undefined> =>
134
+ Bun.file(join(root, rel))
135
+ .text()
136
+ .catch(() => undefined);