@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.
- package/CLAUDE.md +36 -13
- package/README.md +31 -8
- package/package.json +29 -28
- package/src/admin-catalog-keys.ts +16 -0
- package/src/admin-registration.ts +192 -0
- package/src/affected.ts +27 -35
- package/src/api-registration.ts +125 -11
- package/src/app-artifacts.ts +1 -1
- package/src/app-auth.ts +4 -27
- package/src/app-boundaries.ts +6 -20
- package/src/app-config-load.ts +51 -0
- package/src/app-env.ts +5 -11
- package/src/app-load.ts +55 -11
- package/src/app-manifest.ts +16 -8
- package/src/app-mcp.ts +10 -23
- package/src/app-permissions-borrowed.ts +129 -0
- package/src/app-permissions-site.ts +24 -0
- package/src/app-permissions.ts +0 -0
- package/src/app-reload-graph.ts +7 -30
- package/src/app-transport.ts +219 -0
- package/src/boundary-findings.ts +16 -4
- package/src/browser-transport.ts +160 -0
- package/src/budgets.ts +27 -34
- package/src/cdp-shot-driver.ts +3 -1
- package/src/cmd-auth-spec.ts +16 -0
- package/src/cmd-auth.ts +65 -0
- package/src/cmd-build-spec.ts +10 -3
- package/src/cmd-build.ts +107 -11
- package/src/cmd-db-backfill.ts +25 -12
- package/src/cmd-db.ts +66 -16
- package/src/cmd-deploy-helm.ts +7 -6
- package/src/cmd-deploy.ts +4 -4
- package/src/cmd-dev.ts +9 -8
- package/src/cmd-doctor-spec.ts +2 -9
- package/src/cmd-doctor.ts +75 -24
- package/src/cmd-env.ts +26 -2
- package/src/cmd-generate.ts +195 -47
- package/src/cmd-help.ts +19 -6
- package/src/cmd-i18n.ts +10 -9
- package/src/cmd-jobs-spec.ts +19 -3
- package/src/cmd-jobs.ts +136 -7
- package/src/cmd-manifest-spec.ts +1 -1
- package/src/cmd-manifest.ts +6 -25
- package/src/cmd-mcp.ts +46 -15
- package/src/cmd-new.ts +15 -1
- package/src/cmd-planned.ts +15 -8
- package/src/cmd-routes.ts +27 -3
- package/src/cmd-secrets-ring.ts +76 -0
- package/src/cmd-secrets-spec.ts +10 -2
- package/src/cmd-secrets.ts +95 -18
- package/src/cmd-shot-matrix.ts +1 -3
- package/src/cmd-shot.ts +12 -3
- package/src/cmd-tasks.ts +84 -15
- package/src/cmd-test-spec.ts +1 -7
- package/src/cmd-test.ts +13 -29
- package/src/cmd-verify.ts +13 -5
- package/src/comment-safe.ts +16 -0
- package/src/coverage-floor.ts +334 -0
- package/src/coverage-lcov.ts +193 -0
- package/src/coverage-source.ts +136 -0
- package/src/db-object-drift.ts +30 -0
- package/src/db-replay-engine.ts +115 -0
- package/src/db-schema-dump.ts +150 -0
- package/src/db-schema-refresh.ts +122 -0
- package/src/db-subscribes.ts +12 -4
- package/src/dev-dashboard-guard.ts +76 -0
- package/src/dev-dashboard.ts +51 -29
- package/src/dev-lock.ts +4 -7
- package/src/dev-route-table.ts +9 -0
- package/src/dispatch.ts +3 -6
- package/src/doctor-app-url.ts +39 -0
- package/src/doctor-auth.ts +95 -0
- package/src/doctor-coverage.ts +39 -0
- package/src/doctor-guards.ts +54 -0
- package/src/doctor-sealed.ts +132 -0
- package/src/document-graph.ts +82 -0
- package/src/drift-replica-identity.ts +42 -0
- package/src/error-catalog.ts +6 -9
- package/src/error-codes.ts +55 -3
- package/src/error-unthrown.ts +29 -11
- package/src/errors.ts +54 -83
- package/src/framework-schema-apply.ts +78 -0
- package/src/framework-schema-stamp.ts +96 -0
- package/src/framework-schema.ts +28 -2
- package/src/generate-files.ts +79 -12
- package/src/generate-grant-findings.ts +76 -0
- package/src/generate-grants.ts +77 -3
- package/src/generate-kinds.ts +52 -1
- package/src/generate-shadow.ts +46 -0
- package/src/generate-write.ts +62 -40
- package/src/generated-imports.ts +59 -0
- package/src/gh-target.ts +12 -7
- package/src/guard-sources.ts +104 -0
- package/src/guards.ts +23 -7
- package/src/handle-registration.ts +217 -0
- package/src/i18n-audit.ts +4 -1
- package/src/i18n-registration.ts +36 -0
- package/src/image-prepare.ts +51 -8
- package/src/import-closure.ts +165 -0
- package/src/index.ts +12 -5
- package/src/island-bundle.ts +3 -15
- package/src/island-store.ts +24 -16
- package/src/jobs-drain.ts +95 -21
- package/src/jobs-json.ts +50 -0
- package/src/jobs-report.ts +7 -4
- package/src/json-merge.ts +46 -0
- package/src/loopback-host.ts +13 -0
- package/src/manifest-staleness.ts +62 -0
- package/src/mcp-errors.ts +41 -4
- package/src/mcp-host.ts +16 -9
- package/src/mcp-test-run.ts +38 -0
- package/src/messages.ts +35 -0
- package/src/metrics-endpoint.ts +95 -24
- package/src/migration-extensions.ts +29 -0
- package/src/module-imports.ts +47 -0
- package/src/output.ts +24 -9
- package/src/page-navigation.ts +7 -23
- package/src/page-speculation.ts +5 -20
- package/src/permission-grants.ts +41 -9
- package/src/prerender-out.ts +34 -7
- package/src/prerender.ts +8 -5
- package/src/publish-closure.ts +101 -0
- package/src/pwa-artifacts.ts +26 -94
- package/src/registry.ts +2 -0
- package/src/revalidated-response.ts +4 -3
- package/src/role-load.ts +143 -0
- package/src/role-replicator.ts +16 -0
- package/src/role-start.ts +27 -5
- package/src/role-sync.ts +55 -20
- package/src/role-wake.ts +45 -0
- package/src/runtime-admin.ts +141 -0
- package/src/runtime-assets.ts +11 -7
- package/src/runtime-bindings.ts +4 -4
- package/src/runtime-cache.ts +38 -47
- package/src/runtime-hooks.ts +5 -2
- package/src/runtime-idempotency-scope.ts +23 -0
- package/src/runtime-isr.ts +36 -0
- package/src/runtime-jobs.ts +8 -33
- package/src/runtime-mfa-warning.ts +29 -0
- package/src/runtime-notify-retention.ts +10 -46
- package/src/runtime-overrides.ts +1 -1
- package/src/runtime-purge.ts +38 -16
- package/src/runtime-queue.ts +39 -11
- package/src/runtime-realtime.ts +9 -45
- package/src/runtime-render.ts +68 -35
- package/src/runtime-services.ts +22 -18
- package/src/runtime-storage.ts +10 -5
- package/src/scaffold-typecheck.ts +1 -0
- package/src/schema-diff.ts +16 -0
- package/src/schema-drift.ts +45 -15
- package/src/schema-dump-drift.ts +64 -0
- package/src/seo-routes.ts +80 -9
- package/src/serve-boot.ts +114 -158
- package/src/serve-drain.ts +9 -33
- package/src/serve-env.ts +9 -6
- package/src/serve-prebuilt-paths.ts +19 -0
- package/src/serve-prebuilt.ts +84 -0
- package/src/serve-web.ts +169 -0
- package/src/serve.ts +6 -9
- package/src/server-barrels.ts +54 -0
- package/src/shot-locale.ts +5 -23
- package/src/site-assets.ts +3 -5
- package/src/site-config.ts +9 -48
- package/src/site-seo.ts +1 -1
- package/src/sitemap-lastmod.ts +5 -0
- package/src/solid-loader.ts +12 -3
- package/src/source-edit.ts +49 -0
- package/src/source-files.ts +6 -0
- package/src/stored-object-headers.ts +43 -0
- package/src/sync-url.ts +2 -2
- package/src/tasks-facts.ts +3 -26
- package/src/templates/action.ts +147 -11
- package/src/templates/admin-catalog.ts +58 -0
- package/src/templates/admin-page.ts +36 -8
- package/src/templates/admin.ts +57 -17
- package/src/templates/backfill.ts +121 -87
- package/src/templates/entity.ts +140 -44
- package/src/templates/guard-animated-layout-property.ts +4 -11
- package/src/templates/guard-bare-error.ts +9 -13
- package/src/templates/guard-focus-visible.ts +4 -11
- package/src/templates/guard-image-dimensions.ts +6 -6
- package/src/templates/guard-island-without-states.ts +2 -4
- package/src/templates/guard-raw-breakpoint.ts +210 -0
- package/src/templates/guard-raw-colour.ts +4 -11
- package/src/templates/guard-raw-length.ts +375 -0
- package/src/templates/guard-raw-motion.ts +213 -0
- package/src/templates/guard-raw-shadow.ts +187 -0
- package/src/templates/guard-raw-z-index.ts +166 -0
- package/src/templates/guard-repo-raw-sql.ts +345 -0
- package/src/templates/guard-semantic-interactive.ts +6 -6
- package/src/templates/guard-undeclared-custom-property.ts +217 -0
- package/src/templates/guard-undefined-style-class.ts +215 -0
- package/src/templates/guard-untranslated-string.ts +6 -6
- package/src/templates/guard-unzoned-date.ts +9 -13
- package/src/templates/index.ts +3 -2
- package/src/templates/island.ts +34 -51
- package/src/templates/job-body-test.ts +121 -0
- package/src/templates/job.ts +23 -8
- package/src/templates/naming.ts +28 -0
- package/src/templates/policy.ts +16 -9
- package/src/templates/query.ts +242 -26
- package/src/templates/resource-create.ts +62 -18
- package/src/templates/resource-form-island.ts +58 -80
- package/src/templates/resource-page.ts +269 -0
- package/src/templates/resource-service.ts +96 -0
- package/src/templates/resource.ts +86 -61
- package/src/templates/route.ts +41 -35
- package/src/templates/scaffold-api.ts +55 -1
- package/src/templates/scaffold-app.ts +177 -53
- package/src/templates/scaffold-auth.ts +83 -19
- package/src/templates/scaffold-browser-client.ts +90 -0
- package/src/templates/scaffold-container-compose.ts +10 -1
- package/src/templates/scaffold-container.ts +20 -2
- package/src/templates/scaffold-dashboard-bare.ts +64 -2
- package/src/templates/scaffold-dashboard-example.ts +124 -31
- package/src/templates/scaffold-dashboard-shared.ts +1 -20
- package/src/templates/scaffold-dashboard.ts +1 -2
- package/src/templates/scaffold-db-client.ts +144 -0
- package/src/templates/scaffold-db-package.ts +90 -8
- package/src/templates/scaffold-docs.ts +50 -5
- package/src/templates/scaffold-guards.ts +54 -11
- package/src/templates/scaffold-helm-fences.ts +76 -0
- package/src/templates/scaffold-helm-templates.ts +32 -7
- package/src/templates/scaffold-helm.ts +31 -1
- package/src/templates/scaffold-http.ts +9 -7
- package/src/templates/scaffold-repo.ts +61 -3
- package/src/templates/scaffold-roles.ts +56 -9
- package/src/templates/scaffold-shell.ts +74 -105
- package/src/templates/scaffold-site.ts +45 -19
- package/src/templates/scaffold-ui-package.ts +18 -0
- package/src/templates/slice-foundation.ts +46 -8
- package/src/test-counts.ts +14 -2
- package/src/test-ignores.ts +25 -0
- package/src/test-passes.ts +0 -15
- package/src/test-select.ts +5 -14
- package/src/test-shards.ts +41 -93
- package/src/test-workers.ts +26 -4
- package/src/theme-boot.ts +6 -31
- package/src/transport-calls.ts +105 -0
- package/src/trusted-hops-error.ts +17 -0
- package/src/unscanned-admin.ts +54 -0
- package/src/unthrown-codes.ts +26 -0
- package/src/verify-checks.ts +43 -53
- package/src/verify-coverage-run.ts +190 -0
- package/src/verify-deadline.ts +353 -0
- package/src/verify-floor.ts +36 -9
- package/src/verify-merge.ts +171 -45
- package/src/verify-part-step.ts +90 -0
- package/src/verify-progress.ts +25 -0
- package/src/verify-role-load-probe.ts +54 -0
- package/src/verify-role-load.ts +148 -0
- package/src/verify-run.ts +116 -37
- package/src/verify-shard.ts +33 -11
- package/src/verify-stalled.ts +152 -0
- package/src/verify-step.ts +42 -1
- package/src/verify-tests.ts +83 -8
- package/src/workspace-checks.ts +50 -8
- package/src/workspace-dep-edit.ts +127 -0
- package/src/build-errors.ts +0 -54
- package/src/cmd-dev-fixture.ts +0 -204
- package/src/dev-live-fixture.ts +0 -122
- package/src/policy-fixture.ts +0 -78
- package/src/realtime-browser-probe-fixture.ts +0 -9
- package/src/role-start-fixture.ts +0 -78
- package/src/scaffold-fixture.ts +0 -160
- package/src/templates/island-fixture.ts +0 -76
- package/src/verify-errors.ts +0 -49
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// The four ways browser code opens its own connection, found in ONE file's text: a `fetch(` CALL,
|
|
2
|
+
// `new WebSocket(`, `new XMLHttpRequest(`, `new EventSource(`. Pure, so every false positive the
|
|
3
|
+
// rule has to leave alone is a fixture string. Which file may hold which shape is the caller's.
|
|
4
|
+
|
|
5
|
+
import { maskLiterals } from '@ultimat3/core';
|
|
6
|
+
import { lineIndex } from './ts-scan';
|
|
7
|
+
|
|
8
|
+
export type TransportShape = 'fetch' | 'websocket' | 'xhr' | 'eventsource';
|
|
9
|
+
|
|
10
|
+
export interface TransportCall {
|
|
11
|
+
readonly shape: TransportShape;
|
|
12
|
+
readonly line: number;
|
|
13
|
+
/** What the source spells — `fetch`, `globalThis.fetch`, `WebSocket` — so a finding quotes it. */
|
|
14
|
+
readonly spelled: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const CONSTRUCTED: ReadonlyMap<string, TransportShape> = new Map([
|
|
18
|
+
['WebSocket', 'websocket'],
|
|
19
|
+
['XMLHttpRequest', 'xhr'],
|
|
20
|
+
['EventSource', 'eventsource'],
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
/** The global object's three spellings. `server.fetch(` is a method, never the global. */
|
|
24
|
+
const GLOBAL = String.raw`(?:(?:globalThis|window|self)\s*\.\s*)`;
|
|
25
|
+
const NEW = new RegExp(
|
|
26
|
+
String.raw`\bnew\s+${GLOBAL}?(WebSocket|XMLHttpRequest|EventSource)\s*\(`,
|
|
27
|
+
'g',
|
|
28
|
+
);
|
|
29
|
+
const FETCH = new RegExp(String.raw`(?<![\w$.])(${GLOBAL}?)fetch\s*\(`, 'g');
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Whether the file binds `fetch` itself — a `const`/`let`/`var`/`function`, an import, a
|
|
33
|
+
* destructured declaration or a parameter — which makes a BARE `fetch(` its own function, not the
|
|
34
|
+
* page's. `packages/action/src/idempotency-postgres.ts` calls a local `fetch(key)`. Per file, not
|
|
35
|
+
* per scope: a floor, and the one direction it errs is silence on a file that shadows the global
|
|
36
|
+
* AND calls it bare. `globalThis.fetch(` is reported regardless.
|
|
37
|
+
*/
|
|
38
|
+
export function bindsFetch(code: string): boolean {
|
|
39
|
+
return (
|
|
40
|
+
/\b(?:const|let|var|function)\s+fetch\b/.test(code) ||
|
|
41
|
+
/\bimport\s+[^;]*\bfetch\b[^;]*\bfrom\b/.test(code) ||
|
|
42
|
+
/\b(?:const|let|var)\s*\{[^}]*\bfetch\b[^}]*\}\s*=/.test(code) ||
|
|
43
|
+
/\(\s*(?:[\w$]+\s*(?::[^,()]+)?,\s*)*fetch\s*[?:,)=][^()]*\)\s*(?::[^={;]+)?(?:=>|\{)/.test(
|
|
44
|
+
code,
|
|
45
|
+
)
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The `)` closing the `(` at `open`, or `-1`. A bare depth count is exact HERE and only here: the
|
|
51
|
+
* text is `maskLiterals`' output, where every string, template, comment and regex body is already
|
|
52
|
+
* blank — so each parenthesis left is code.
|
|
53
|
+
*/
|
|
54
|
+
function closeOf(masked: string, open: number): number {
|
|
55
|
+
let depth = 0;
|
|
56
|
+
for (let index = open; index < masked.length; index += 1) {
|
|
57
|
+
if (masked[index] === '(') depth += 1;
|
|
58
|
+
else if (masked[index] === ')') {
|
|
59
|
+
depth -= 1;
|
|
60
|
+
if (depth === 0) return index;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return -1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** `fetch(req) {` and `fetch(req): Promise<Response> {` define a method; they call nothing. */
|
|
67
|
+
function isDefinition(masked: string, open: number, start: number): boolean {
|
|
68
|
+
if (/\b(?:function|async|get|set)\s*$/.test(masked.slice(Math.max(0, start - 16), start))) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
const close = closeOf(masked, open);
|
|
72
|
+
if (close < 0) return false;
|
|
73
|
+
const after = masked.slice(close + 1).trimStart();
|
|
74
|
+
return after.startsWith('{') || (after.startsWith(':') && !after.startsWith('::'));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** A file naming none of the four cannot call one — and masking it is the expensive half. */
|
|
78
|
+
const NAMES_A_TRANSPORT = /fetch|WebSocket|XMLHttpRequest|EventSource/;
|
|
79
|
+
|
|
80
|
+
export function transportCalls(source: string): readonly TransportCall[] {
|
|
81
|
+
if (!NAMES_A_TRANSPORT.test(source)) return [];
|
|
82
|
+
// Comments AND string contents blanked, offsets kept: `'fetch('` in a message is not a call, and
|
|
83
|
+
// neither is the one a template-literal scaffold emits for somebody else's file.
|
|
84
|
+
const masked = maskLiterals(source);
|
|
85
|
+
const lineAt = lineIndex(masked);
|
|
86
|
+
const calls: TransportCall[] = [];
|
|
87
|
+
for (const found of masked.matchAll(NEW)) {
|
|
88
|
+
const name = found[1] ?? '';
|
|
89
|
+
const shape = CONSTRUCTED.get(name);
|
|
90
|
+
if (shape !== undefined) calls.push({ shape, line: lineAt(found.index), spelled: name });
|
|
91
|
+
}
|
|
92
|
+
const local = bindsFetch(masked);
|
|
93
|
+
for (const found of masked.matchAll(FETCH)) {
|
|
94
|
+
const qualified = (found[1] ?? '').length > 0;
|
|
95
|
+
if (!qualified && local) continue;
|
|
96
|
+
const open = found.index + found[0].length - 1;
|
|
97
|
+
if (!qualified && isDefinition(masked, open, found.index)) continue;
|
|
98
|
+
calls.push({
|
|
99
|
+
shape: 'fetch',
|
|
100
|
+
line: lineAt(found.index),
|
|
101
|
+
spelled: qualified ? `${(found[1] ?? '').replace(/\s/g, '')}fetch` : 'fetch',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return calls.sort((a, b) => a.line - b.line);
|
|
105
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// The refusal of a malformed `TRUSTED_PROXY_HOPS` — its own code, never `X_PORT_INVALID`: a proxy
|
|
2
|
+
// count is not a port, and an operator told "is not a TCP port number" looked for the wrong setting.
|
|
3
|
+
|
|
4
|
+
import { UltimateError } from '@ultimat3/core';
|
|
5
|
+
import { MAX_PROXY_HOPS } from '@ultimat3/http';
|
|
6
|
+
import { quoteArg } from './shell-quote';
|
|
7
|
+
|
|
8
|
+
export class TrustedProxyHopsInvalidError extends UltimateError {
|
|
9
|
+
constructor(input: { readonly value: string }) {
|
|
10
|
+
super({
|
|
11
|
+
code: 'X_TRUSTED_PROXY_HOPS_INVALID',
|
|
12
|
+
cause: `TRUSTED_PROXY_HOPS=${quoteArg(input.value)} is not a whole number of proxies from 1 to ${MAX_PROXY_HOPS} — the count that append to x-forwarded-for in front of this process; unset means none`,
|
|
13
|
+
fix: 'TRUSTED_PROXY_HOPS=1 ROLE=web bun apps/web/server.ts',
|
|
14
|
+
meta: { value: input.value, max: MAX_PROXY_HOPS },
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// A `defineAdmin()` the app scan never imports. The admin's one home is `apps/admin/app/admin/`
|
|
2
|
+
// (`ADMIN_FILE`, what `x new` and `x g admin:page` write); `apps/<app>/src/` is outside every
|
|
3
|
+
// glob `app-load.ts` imports, so a declaration there never runs and `/admin` is never mounted —
|
|
4
|
+
// silently, which is how the reference app shipped an admin nobody could open.
|
|
5
|
+
|
|
6
|
+
import { renderFixShellArg, stripComments } from '@ultimat3/core';
|
|
7
|
+
import { describeRoutes } from '@ultimat3/render';
|
|
8
|
+
import { ADMIN_FILE } from './admin-registration';
|
|
9
|
+
import type { Finding } from './output';
|
|
10
|
+
import { hasPathSegment } from './path-segments';
|
|
11
|
+
import { isTest } from './source-files';
|
|
12
|
+
|
|
13
|
+
/** The one place an admin used to be written that the scan cannot see. */
|
|
14
|
+
const UNSCANNED = 'apps/*/src/**/*.{ts,tsx}';
|
|
15
|
+
|
|
16
|
+
const DECLARES = /\bdefineAdmin\(/;
|
|
17
|
+
|
|
18
|
+
/** Any admin this process mounted — then every declaration was reached, wherever it sits. */
|
|
19
|
+
export const adminMounted = (): boolean =>
|
|
20
|
+
describeRoutes().some((route) => route.mount?.by === 'defineAdmin');
|
|
21
|
+
|
|
22
|
+
/** Where the file belongs: its own name under the admin's home, `index.ts` as the declaration. */
|
|
23
|
+
const homeOf = (file: string): string => {
|
|
24
|
+
const name = file.slice(file.lastIndexOf('/') + 1);
|
|
25
|
+
const home = ADMIN_FILE.slice(0, ADMIN_FILE.lastIndexOf('/'));
|
|
26
|
+
return /^index\.tsx?$/.test(name) ? ADMIN_FILE : `${home}/${name}`;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const unscannedFinding = (file: string): Finding => ({
|
|
30
|
+
code: 'X_ADMIN_UNSCANNED',
|
|
31
|
+
cause: `${file} calls defineAdmin(), and the app scan imports apps/*/{site,app,api,shared}/** only — no boot evaluates it, so the admin is never mounted and /admin answers 404`,
|
|
32
|
+
// A file name is the app's: screened, so a name carrying shell syntax reads as a placeholder.
|
|
33
|
+
fix: `git mv ${renderFixShellArg(file, '<the file above>')} ${renderFixShellArg(homeOf(file), '<apps/admin/app/admin/its-name.ts>')} # then repoint its relative imports and x verify --only manifest --json`,
|
|
34
|
+
at: file,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* One finding per unscanned file that declares an admin. Read only when nothing mounted one: an app
|
|
39
|
+
* whose admin is mounted pays for no glob, and comments are masked so prose naming the call is not
|
|
40
|
+
* a declaration.
|
|
41
|
+
*/
|
|
42
|
+
export async function unscannedAdminFindings(
|
|
43
|
+
root: string,
|
|
44
|
+
mounted: () => boolean = adminMounted,
|
|
45
|
+
): Promise<readonly Finding[]> {
|
|
46
|
+
if (mounted()) return [];
|
|
47
|
+
const files: string[] = [];
|
|
48
|
+
for await (const file of new Bun.Glob(UNSCANNED).scan({ cwd: root })) {
|
|
49
|
+
const path = file.replaceAll('\\', '/');
|
|
50
|
+
if (hasPathSegment(path, 'node_modules') || isTest(path)) continue;
|
|
51
|
+
if (DECLARES.test(stripComments(await Bun.file(`${root}/${path}`).text()))) files.push(path);
|
|
52
|
+
}
|
|
53
|
+
return files.sort().map(unscannedFinding);
|
|
54
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// The registered codes this repo no longer throws, as a list `error-unthrown.ts` reads — its own
|
|
2
|
+
// file, so the literals naming the waived codes are not mistaken for a throw site.
|
|
3
|
+
|
|
4
|
+
/** Where the list lives, so a finding can name the file to edit. */
|
|
5
|
+
export const UNTHROWN_CODES_FILE = 'packages/cli/src/unthrown-codes.ts';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The codes this repo registers and no longer throws — each a decision, and each row on the
|
|
9
|
+
* reference page says so in words. A LIST, never a prose match: the waiver was any row containing
|
|
10
|
+
* "not thrown" or "thrown by nothing", so a sentence that happened to use either phrase silenced the
|
|
11
|
+
* rule for its code. A listing whose code is thrown again, or is not registered, is
|
|
12
|
+
* `X_ERROR_CODE_UNTHROWN_STALE`.
|
|
13
|
+
*/
|
|
14
|
+
export const UNTHROWN_CODES: ReadonlySet<string> = new Set([
|
|
15
|
+
// `x db studio` is planned and unreleased; it exits X_NOT_IMPLEMENTED.
|
|
16
|
+
'X_DB_STUDIO_FAILED',
|
|
17
|
+
// 21.0.0: there is no LiveClient to register (X_REALTIME_UNINSTALLED replaced it).
|
|
18
|
+
'X_LIVE_CLIENT_MISSING',
|
|
19
|
+
// 21.0.0: liveHookFor was deleted; useQuery reads any query.
|
|
20
|
+
'X_QUERY_NOT_SUBSCRIBABLE',
|
|
21
|
+
// 21.0.0: the worker no longer POSTs a flush endpoint nothing mounted.
|
|
22
|
+
'X_PWA_SYNC_FLUSH_FAILED',
|
|
23
|
+
'X_PWA_SYNC_INCOMPLETE',
|
|
24
|
+
// 24.0.0: `x test --worker` was deleted; `x verify --only unit --shard i/n` is the one split.
|
|
25
|
+
'X_TEST_SHARD_FAILED',
|
|
26
|
+
]);
|
package/src/verify-checks.ts
CHANGED
|
@@ -6,18 +6,13 @@ import { existsSync } from 'node:fs';
|
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
8
8
|
import type { Manifest } from '@ultimat3/manifest';
|
|
9
|
-
import {
|
|
10
|
-
AGENTS_MD_FILENAME,
|
|
11
|
-
assertNoDrift,
|
|
12
|
-
MANIFEST_FILENAME,
|
|
13
|
-
verifyContract,
|
|
14
|
-
} from '@ultimat3/manifest';
|
|
9
|
+
import { AGENTS_MD_FILENAME, MANIFEST_FILENAME, verifyContract } from '@ultimat3/manifest';
|
|
15
10
|
import type { MetaIssue } from '@ultimat3/seo';
|
|
16
11
|
import { validateMeta } from '@ultimat3/seo';
|
|
17
12
|
import { checkAgentsMd } from './app-agents-md';
|
|
13
|
+
import { readAppSources } from './app-boundaries';
|
|
18
14
|
import { envExampleFindings } from './app-env';
|
|
19
15
|
import { appManifest, readAppManifest } from './app-manifest';
|
|
20
|
-
import { OPENAPI_FILE, openApiStaleness } from './app-openapi';
|
|
21
16
|
import { policyFindings } from './app-permissions';
|
|
22
17
|
import { APP_CONFIG_FILE } from './app-root';
|
|
23
18
|
import { asyncPageFindings } from './async-pages';
|
|
@@ -32,14 +27,21 @@ import { catalogFindings } from './i18n-registration';
|
|
|
32
27
|
import { unregisteredJobFindings } from './job-registration';
|
|
33
28
|
import { liveRouteFindings } from './live-routes';
|
|
34
29
|
import { withLoadFindings, withoutLoadFindings } from './load-findings';
|
|
30
|
+
import {
|
|
31
|
+
hasCommittedContract,
|
|
32
|
+
manifestMissingFindings,
|
|
33
|
+
manifestStaleness,
|
|
34
|
+
} from './manifest-staleness';
|
|
35
35
|
import { msg } from './messages';
|
|
36
36
|
import type { Finding } from './output';
|
|
37
37
|
import { findingFrom } from './output';
|
|
38
38
|
import { checkMigrationDrift } from './schema-drift';
|
|
39
|
+
import { checkSchemaDump } from './schema-dump-drift';
|
|
39
40
|
import { scanSiteMeta } from './seo-meta';
|
|
40
41
|
import { quoteArg } from './shell-quote';
|
|
41
42
|
import { readStaticReport } from './static-report';
|
|
42
43
|
import { floorProblemFindings, readVerifyFloor } from './verify-floor';
|
|
44
|
+
import { roleLoadFindings } from './verify-role-load';
|
|
43
45
|
import type { VerifyStep } from './verify-step';
|
|
44
46
|
import { fromExec, fromFindings, hostFindings } from './verify-step';
|
|
45
47
|
import { TEST_STEPS } from './verify-tests';
|
|
@@ -90,7 +92,8 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
90
92
|
},
|
|
91
93
|
{
|
|
92
94
|
name: 'boundaries',
|
|
93
|
-
summary:
|
|
95
|
+
summary:
|
|
96
|
+
"surface, layer and package-tier imports, one browser transport, and the app's own guards",
|
|
94
97
|
// An app's `guards/` rides here rather than becoming a step of its own, for the reason the
|
|
95
98
|
// seam already states: a host adds findings to a step, it can never add, remove, reorder or
|
|
96
99
|
// skip one — so "green" keeps meaning exactly what it meant. This is the step whose host slot
|
|
@@ -101,12 +104,18 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
101
104
|
// Discovered, not registered: `guardFindings` reads the directory. A guard that had to
|
|
102
105
|
// announce itself is a guard an app can forget to announce, which is the coupling axiom 8's
|
|
103
106
|
// extension model rejects.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
//
|
|
108
|
+
// ONE read of the app's surface files for the whole step: the import rules, the transport
|
|
109
|
+
// rule and every guard are handed the same text. The guards used to walk and read the tree a
|
|
110
|
+
// second time (`guard-sources.ts`).
|
|
111
|
+
async run(ctx) {
|
|
112
|
+
const sources = await readAppSources(ctx.root);
|
|
113
|
+
return fromFindings([
|
|
114
|
+
...(await appBoundaryFindings(ctx.root, sources)),
|
|
115
|
+
...(await guardFindings(ctx.root, sources)),
|
|
108
116
|
...(await hostFindings(ctx, 'boundaries')),
|
|
109
|
-
])
|
|
117
|
+
]);
|
|
118
|
+
},
|
|
110
119
|
},
|
|
111
120
|
{
|
|
112
121
|
name: 'filesize',
|
|
@@ -161,7 +170,7 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
161
170
|
{
|
|
162
171
|
name: 'drift',
|
|
163
172
|
summary:
|
|
164
|
-
'entity declarations vs migrations, every destructive statement declared,
|
|
173
|
+
'entity declarations vs migrations, every destructive statement declared, every statement no declaration carries, and the committed schema dump',
|
|
165
174
|
// Only an app owns migrations; a package monorepo's `packages/db` is the driver, not a schema.
|
|
166
175
|
// Source, not database: the gate runs in CI with nothing listening, and the database half is
|
|
167
176
|
// the post-migrate verification `runMigrations` performs where a connection is already open.
|
|
@@ -179,15 +188,20 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
179
188
|
// a squash silently drops. Same directory, same reader, no database — this step's own
|
|
180
189
|
// question, which is why it is not a step of its own.
|
|
181
190
|
...(await checkUngeneratableMigrations(ctx.root)),
|
|
191
|
+
// The fourth, and the only one that opens a database — a scratch embedded one it boots
|
|
192
|
+
// itself, so CI still needs nothing listening: the committed `packages/db/schema/` against
|
|
193
|
+
// what replaying the migrations produces, and that dump loaded back (`X_SCHEMA_DUMP_DRIFT`).
|
|
194
|
+
...(await checkSchemaDump(ctx.root, ctx.env ?? Bun.env)),
|
|
182
195
|
]),
|
|
183
196
|
},
|
|
184
197
|
{
|
|
185
198
|
name: 'contract-diff',
|
|
186
199
|
summary: 'the published contract vs the committed manifest',
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
200
|
+
// A BREAKING change against the committed manifest, and nothing else. Whether the committed
|
|
201
|
+
// files are stale — `openapi.json` included — is the `manifest` step's, through the one check
|
|
202
|
+
// `x manifest --check` runs (`manifest-staleness.ts`): reported here as well it was two
|
|
203
|
+
// steps and one command answering one question, and the step named `manifest` saying green.
|
|
204
|
+
applies: async (ctx) => existsSync(join(ctx.root, MANIFEST_FILENAME)),
|
|
191
205
|
async run(ctx) {
|
|
192
206
|
const committed = await readAppManifest(ctx.root);
|
|
193
207
|
const { manifest, findings } = await appManifest(ctx.root);
|
|
@@ -195,14 +209,13 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
195
209
|
return withoutLoadFindings(ctx.root, [
|
|
196
210
|
...findings,
|
|
197
211
|
...(committed === undefined ? [] : contractFindings(committed, manifest)),
|
|
198
|
-
...(await openApiStaleness(ctx.root, manifest)),
|
|
199
212
|
]);
|
|
200
213
|
},
|
|
201
214
|
},
|
|
202
215
|
{
|
|
203
216
|
name: 'budgets',
|
|
204
217
|
summary:
|
|
205
|
-
'per-route JS bytes
|
|
218
|
+
'per-route JS bytes, the global style layer every document carries, and the routes that boot nothing to receive their live rows',
|
|
206
219
|
// The global-style assertion rides here rather than becoming a step of its own, because this
|
|
207
220
|
// step already asks the one question it asks: what does the document this build emits actually
|
|
208
221
|
// contain? It is also the same app load — `appManifest` fills render's stylesheet registry on
|
|
@@ -328,14 +341,18 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
328
341
|
const agents = await checkAgentsMd(ctx.root, floor?.agentsMdMaxBytes);
|
|
329
342
|
// The app's load failures, once, here — every other step that loaded it points at this one.
|
|
330
343
|
const app = existsSync(join(ctx.root, APP_CONFIG_FILE));
|
|
344
|
+
// Started first and awaited last: it is a child process, and it runs beside the rest.
|
|
345
|
+
const roleLoad = app ? roleLoadFindings(ctx.root, ctx.runner) : Promise.resolve([]);
|
|
331
346
|
const drift = await driftFindings(ctx.root);
|
|
332
347
|
const findings = [
|
|
333
|
-
...manifestMissingFindings(ctx.root),
|
|
334
348
|
...(app ? await withLoadFindings(ctx.root, drift) : drift),
|
|
335
349
|
// An app only: the framework monorepo declares no `defineApi`, so it has no list to be in.
|
|
336
350
|
...(existsSync(join(ctx.root, APP_CONFIG_FILE))
|
|
337
351
|
? await unregisteredJobFindings(ctx.root)
|
|
338
352
|
: []),
|
|
353
|
+
// What a worker never registers: a declaration beside a component the API index does not
|
|
354
|
+
// import (`verify-role-load.ts`). The boot checks jobs and tasks only, and only at boot.
|
|
355
|
+
...(await roleLoad),
|
|
339
356
|
...(await envExampleFindings(ctx.root)),
|
|
340
357
|
...floorProblemFindings(floor),
|
|
341
358
|
...agents.findings,
|
|
@@ -365,41 +382,14 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
365
382
|
];
|
|
366
383
|
|
|
367
384
|
/**
|
|
368
|
-
* The
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
* thing that writes it. Nothing ran it: after `x new`, `bin/setup` and all thirteen generators,
|
|
372
|
-
* `find . -name '*.manifest.json'` returned nothing while this step reported green (#F7).
|
|
373
|
-
*
|
|
374
|
-
* An app root, never this repo: the framework monorepo emits `framework.manifest.json` and has no
|
|
375
|
-
* `x.manifest.json` to be missing, so the `app.config.ts` is what decides — the same discriminator
|
|
376
|
-
* `drift`, `budgets`, `seo`, `i18n` and `policy` each use.
|
|
385
|
+
* The committed generated files against the code — `x manifest --check`'s check, through its
|
|
386
|
+
* function. With no committed contract there is nothing to load an app for: an app root is told
|
|
387
|
+
* its manifest is missing, and the framework monorepo (which has neither file) is told nothing.
|
|
377
388
|
*/
|
|
378
|
-
function manifestMissingFindings(root: string): readonly Finding[] {
|
|
379
|
-
if (!existsSync(join(root, APP_CONFIG_FILE))) return [];
|
|
380
|
-
if (existsSync(join(root, MANIFEST_FILENAME))) return [];
|
|
381
|
-
return [
|
|
382
|
-
{
|
|
383
|
-
code: 'X_MANIFEST_MISSING',
|
|
384
|
-
cause: `${MANIFEST_FILENAME} does not exist, so every fact an agent reads about this app — route table, action schemas, policies, error codes — is unavailable`,
|
|
385
|
-
fix: 'x manifest',
|
|
386
|
-
docs: ERROR_DOCS_URL,
|
|
387
|
-
at: MANIFEST_FILENAME,
|
|
388
|
-
},
|
|
389
|
-
];
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/** `assertNoDrift` throws `X_MANIFEST_DRIFT`; a step reports, so the error becomes a finding. */
|
|
393
389
|
async function driftFindings(root: string): Promise<readonly Finding[]> {
|
|
394
|
-
|
|
395
|
-
if (!existsSync(path)) return [];
|
|
390
|
+
if (!hasCommittedContract(root)) return manifestMissingFindings(root);
|
|
396
391
|
const { manifest, findings } = await appManifest(root);
|
|
397
|
-
|
|
398
|
-
await assertNoDrift({ manifest, path });
|
|
399
|
-
return findings;
|
|
400
|
-
} catch (error) {
|
|
401
|
-
return [...findings, { ...findingFrom(error), at: MANIFEST_FILENAME }];
|
|
402
|
-
}
|
|
392
|
+
return [...findings, ...(await manifestStaleness(root, manifest))];
|
|
403
393
|
}
|
|
404
394
|
|
|
405
395
|
/** A breaking change is allowed — with a major bump. `verifyContract` is the one that decides. */
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// An app's `unit` step, run so its coverage can be read: the suite as a pool of plain `bun test`
|
|
2
|
+
// processes over fixed slices, each writing its own lcov, folded by `coverage-lcov.ts`'s rule.
|
|
3
|
+
//
|
|
4
|
+
// NOT `bun test --parallel` with `--coverage` added, which is the obvious form and was measured
|
|
5
|
+
// first (2026-10-01, Bun 1.4.0, `examples/dummy`, 53 unit files): Bun's own cross-worker merge is
|
|
6
|
+
// a union, so the SAME tree read 59.9% of lines at four workers, 65.0% at two and 65.5% in one
|
|
7
|
+
// process. A floor that moves with the core count is not a floor. Per-process lcov folded here
|
|
8
|
+
// read 65.90% at 2, 4 and 8 processes.
|
|
9
|
+
//
|
|
10
|
+
// It costs nothing measurable: four slice processes took 9.7 s against 12.4 s for `--parallel=4`
|
|
11
|
+
// over the same files, on a loaded 12-core box.
|
|
12
|
+
|
|
13
|
+
// why: Bun ships no recursive delete and no path-join primitive; the lcov parts live under `.x/`.
|
|
14
|
+
import { rm } from 'node:fs/promises';
|
|
15
|
+
// why: Bun ships no path-join primitive.
|
|
16
|
+
import { dirname, join } from 'node:path';
|
|
17
|
+
import type { CoverageMap } from './coverage-lcov';
|
|
18
|
+
import { fileCoverageOf, mergeCoverage, parseLcov } from './coverage-lcov';
|
|
19
|
+
import type { ExecResult, Runner } from './exec';
|
|
20
|
+
import { execOutput } from './exec';
|
|
21
|
+
import { msg } from './messages';
|
|
22
|
+
import { countsOf } from './test-counts';
|
|
23
|
+
import { testEnvOverrides } from './test-dotenv';
|
|
24
|
+
import { failureOf, ISOLATED_TEST_ENV } from './test-shards';
|
|
25
|
+
import { SLOT_HELD_ENV } from './test-slots';
|
|
26
|
+
import type { StepOutcome } from './verify-step';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Test files per process. Small enough that a process is thrown away before its heap grows with
|
|
30
|
+
* the corpus (`test-batches.ts`), large enough that the ~0.3 s a process costs to start is noise.
|
|
31
|
+
*/
|
|
32
|
+
export const COVERAGE_SLICE_FILES = 16;
|
|
33
|
+
|
|
34
|
+
/** Where a run's lcov parts are written, under the app root — removed when the run has read them. */
|
|
35
|
+
export const COVERAGE_DIR = '.x/coverage';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The slices one run is spent as: a pure function of the sorted file list, never of the machine,
|
|
39
|
+
* so the measured number is the same on a laptop and on a runner. A directory's tests stay in one
|
|
40
|
+
* slice — function coverage is the best one process reached (`mergeFileCoverage`), and the tests
|
|
41
|
+
* that cover a file sit beside it — and only a directory larger than the cap is cut.
|
|
42
|
+
*/
|
|
43
|
+
export function coverageSlices(
|
|
44
|
+
files: readonly string[],
|
|
45
|
+
cap: number = COVERAGE_SLICE_FILES,
|
|
46
|
+
): readonly (readonly string[])[] {
|
|
47
|
+
const byDir = new Map<string, string[]>();
|
|
48
|
+
for (const file of [...new Set(files)].sort()) {
|
|
49
|
+
const dir = dirname(file);
|
|
50
|
+
const list = byDir.get(dir) ?? [];
|
|
51
|
+
list.push(file);
|
|
52
|
+
byDir.set(dir, list);
|
|
53
|
+
}
|
|
54
|
+
const slices: string[][] = [];
|
|
55
|
+
let open: string[] = [];
|
|
56
|
+
const close = (): void => {
|
|
57
|
+
if (open.length > 0) slices.push(open);
|
|
58
|
+
open = [];
|
|
59
|
+
};
|
|
60
|
+
for (const group of byDir.values()) {
|
|
61
|
+
if (group.length > cap) {
|
|
62
|
+
close();
|
|
63
|
+
for (let at = 0; at < group.length; at += cap) slices.push(group.slice(at, at + cap));
|
|
64
|
+
} else {
|
|
65
|
+
if (open.length + group.length > cap) close();
|
|
66
|
+
open.push(...group);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
close();
|
|
70
|
+
return slices;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface CoveredRunOptions {
|
|
74
|
+
readonly root: string;
|
|
75
|
+
readonly runner: Runner;
|
|
76
|
+
/** Paths relative to `root`. */
|
|
77
|
+
readonly files: readonly string[];
|
|
78
|
+
readonly workers: number;
|
|
79
|
+
readonly isolate?: boolean;
|
|
80
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
81
|
+
readonly widthReason?: string;
|
|
82
|
+
readonly lease?: (want: number) => Promise<{ readonly count: number; release(): void }>;
|
|
83
|
+
/** Test files per process. Absent is `COVERAGE_SLICE_FILES`; a test cuts smaller. */
|
|
84
|
+
readonly sliceFiles?: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface CoveredRun extends StepOutcome {
|
|
88
|
+
/** Every file any slice loaded, by the path Bun wrote — relative to `root`. */
|
|
89
|
+
readonly coverage: CoverageMap;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** One slice's argv. An explicit file list, never a re-glob, for `testArgs`' reason. */
|
|
93
|
+
export const coveredArgs = (
|
|
94
|
+
files: readonly string[],
|
|
95
|
+
dir: string,
|
|
96
|
+
isolate: boolean,
|
|
97
|
+
): readonly string[] => [
|
|
98
|
+
'bun',
|
|
99
|
+
'test',
|
|
100
|
+
...(isolate ? ['--isolate'] : []),
|
|
101
|
+
'--coverage',
|
|
102
|
+
'--coverage-reporter=lcov',
|
|
103
|
+
`--coverage-dir=${dir}`,
|
|
104
|
+
...files,
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
export async function runCovered(options: CoveredRunOptions): Promise<CoveredRun> {
|
|
108
|
+
const slices = coverageSlices(options.files, options.sliceFiles);
|
|
109
|
+
const want = Math.max(1, Math.min(Math.trunc(options.workers), slices.length));
|
|
110
|
+
const lease = want > 1 ? await options.lease?.(want) : undefined;
|
|
111
|
+
const width = Math.min(want, lease?.count ?? want);
|
|
112
|
+
const base = join(options.root, COVERAGE_DIR, `unit-${String(process.pid)}`);
|
|
113
|
+
const isolate = options.isolate === true;
|
|
114
|
+
const overrides = {
|
|
115
|
+
...testEnvOverrides(options.root, options.env ?? Bun.env),
|
|
116
|
+
...(isolate ? { [ISOLATED_TEST_ENV]: '1' } : {}),
|
|
117
|
+
...(lease === undefined ? {} : { [SLOT_HELD_ENV]: '1' }),
|
|
118
|
+
};
|
|
119
|
+
const results = new Array<ExecResult>(slices.length);
|
|
120
|
+
let next = 0;
|
|
121
|
+
// Each pool worker names its own numbered test database, as `--parallel`'s workers do.
|
|
122
|
+
const worker = async (slot: number): Promise<void> => {
|
|
123
|
+
while (next < slices.length) {
|
|
124
|
+
const index = next;
|
|
125
|
+
next += 1;
|
|
126
|
+
results[index] = await options.runner(
|
|
127
|
+
coveredArgs(slices[index] as readonly string[], join(base, String(index)), isolate),
|
|
128
|
+
{ cwd: options.root, env: { ...overrides, ULTIMATE_TEST_WORKER: String(slot) } },
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
let maps: CoverageMap[];
|
|
133
|
+
try {
|
|
134
|
+
await Promise.all(Array.from({ length: width }, (_, slot) => worker(slot)));
|
|
135
|
+
maps = await Promise.all(slices.map((_, index) => readPart(join(base, String(index)))));
|
|
136
|
+
} finally {
|
|
137
|
+
lease?.release();
|
|
138
|
+
await rm(base, { recursive: true, force: true });
|
|
139
|
+
}
|
|
140
|
+
const failed = results.filter((result) => !result.ok);
|
|
141
|
+
const first = failed[0];
|
|
142
|
+
const output = results
|
|
143
|
+
.map((result, index) =>
|
|
144
|
+
result.ok
|
|
145
|
+
? undefined
|
|
146
|
+
: [
|
|
147
|
+
msg('cli.test.batch', {
|
|
148
|
+
batch: index + 1,
|
|
149
|
+
batches: results.length,
|
|
150
|
+
files: slices[index]?.length ?? 0,
|
|
151
|
+
}),
|
|
152
|
+
execOutput(result),
|
|
153
|
+
].join('\n'),
|
|
154
|
+
)
|
|
155
|
+
.filter((text) => text !== undefined)
|
|
156
|
+
.join('\n');
|
|
157
|
+
return {
|
|
158
|
+
ok: first === undefined,
|
|
159
|
+
findings:
|
|
160
|
+
first === undefined
|
|
161
|
+
? []
|
|
162
|
+
: [
|
|
163
|
+
failureOf(first.code, options.files.length, {
|
|
164
|
+
workers: width,
|
|
165
|
+
type: 'unit',
|
|
166
|
+
...(isolate ? { isolate: true } : {}),
|
|
167
|
+
}),
|
|
168
|
+
],
|
|
169
|
+
workers: width,
|
|
170
|
+
...(options.widthReason === undefined ? {} : { widthReason: options.widthReason }),
|
|
171
|
+
tests: countsOf(results),
|
|
172
|
+
...(first === undefined ? {} : { output }),
|
|
173
|
+
coverage: mergeCoverage(maps),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* One slice's lcov as a map, kept to the app's own tree: a slice loads a thousand framework files
|
|
179
|
+
* for every hundred of the app's, and none of those is what the floor measures. No file is no
|
|
180
|
+
* coverage — the slice died before writing it, and its exit code already says so.
|
|
181
|
+
*/
|
|
182
|
+
async function readPart(dir: string): Promise<CoverageMap> {
|
|
183
|
+
const file = Bun.file(join(dir, 'lcov.info'));
|
|
184
|
+
if (!(await file.exists())) return {};
|
|
185
|
+
return Object.fromEntries(
|
|
186
|
+
parseLcov(await file.text())
|
|
187
|
+
.filter((record) => /^(?:apps|packages)\//.test(record.file))
|
|
188
|
+
.map((record) => [record.file, fileCoverageOf(record)]),
|
|
189
|
+
);
|
|
190
|
+
}
|