@ultimat3/cli 21.0.0 → 22.1.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 +84 -1650
- package/README.md +40 -2
- package/package.json +30 -30
- package/src/api-registration.ts +124 -0
- package/src/app-artifacts.ts +39 -0
- package/src/app-boundaries.ts +5 -10
- package/src/app-load.ts +37 -5
- package/src/app-openapi.ts +27 -0
- package/src/app-permissions.ts +0 -0
- package/src/app-root.ts +1 -1
- package/src/async-pages.ts +32 -0
- package/src/bin.ts +7 -1
- package/src/boundary-findings.ts +36 -0
- package/src/browser-launcher-fake-html.ts +80 -0
- package/src/browser-launcher-fake.ts +165 -0
- package/src/browser-launcher-port.ts +159 -0
- package/src/browser-launcher.ts +26 -133
- package/src/budgets.ts +11 -1
- package/src/cdp-shot-a11y.ts +77 -0
- package/src/cdp-shot-clock.ts +14 -0
- package/src/cdp-shot-driver.ts +150 -0
- package/src/cdp-shot-element.ts +147 -0
- package/src/cdp-shot-errors.ts +62 -0
- package/src/cdp-shot-keys.ts +152 -0
- package/src/cdp-shot-page.ts +230 -0
- package/src/cdp-shot-watch.ts +241 -0
- package/src/cmd-affected-spec.ts +24 -0
- package/src/cmd-affected.ts +3 -19
- package/src/cmd-build-spec.ts +16 -0
- package/src/cmd-build.ts +46 -20
- package/src/cmd-ci-spec.ts +24 -0
- package/src/cmd-ci.ts +17 -21
- package/src/cmd-db-branch.ts +2 -2
- package/src/cmd-db-spec.ts +99 -0
- package/src/cmd-db.ts +15 -94
- package/src/cmd-deploy-helm.ts +136 -0
- package/src/cmd-deploy-spec.ts +40 -0
- package/src/cmd-deploy.ts +111 -43
- package/src/cmd-dev-spec.ts +25 -0
- package/src/cmd-dev.ts +55 -36
- package/src/cmd-docs-spec.ts +16 -0
- package/src/cmd-docs.ts +2 -11
- package/src/cmd-doctor-spec.ts +21 -0
- package/src/cmd-doctor.ts +4 -18
- package/src/cmd-env-spec.ts +18 -0
- package/src/cmd-env.ts +4 -13
- package/src/cmd-errors-spec.ts +23 -0
- package/src/cmd-errors.ts +3 -17
- package/src/cmd-fix-spec.ts +18 -0
- package/src/cmd-fix.ts +4 -13
- package/src/cmd-generate-spec.ts +31 -0
- package/src/cmd-generate.ts +46 -42
- package/src/cmd-i18n-spec.ts +16 -0
- package/src/cmd-i18n.ts +7 -14
- package/src/cmd-jobs-spec.ts +56 -0
- package/src/cmd-jobs.ts +3 -50
- package/src/cmd-manifest-spec.ts +15 -0
- package/src/cmd-manifest.ts +19 -20
- package/src/cmd-mcp-spec.ts +21 -0
- package/src/cmd-mcp.ts +2 -15
- package/src/cmd-new-spec.ts +33 -0
- package/src/cmd-new.ts +38 -33
- package/src/cmd-policy-spec.ts +13 -0
- package/src/cmd-policy.ts +2 -9
- package/src/cmd-pr-spec.ts +39 -0
- package/src/cmd-pr.ts +5 -65
- package/src/cmd-registries-spec.ts +32 -0
- package/src/cmd-registries.ts +4 -25
- package/src/cmd-routes-spec.ts +12 -0
- package/src/cmd-routes.ts +2 -7
- package/src/cmd-secrets-spec.ts +19 -0
- package/src/cmd-secrets.ts +22 -34
- package/src/cmd-shot-island.ts +12 -13
- package/src/cmd-shot-spec.ts +53 -0
- package/src/cmd-shot.ts +11 -60
- package/src/cmd-tasks-spec.ts +21 -0
- package/src/cmd-tasks.ts +2 -16
- package/src/cmd-test-spec.ts +54 -0
- package/src/cmd-test.ts +4 -49
- package/src/cmd-verify-spec.ts +28 -0
- package/src/cmd-verify.ts +3 -23
- package/src/db-seed.ts +4 -2
- package/src/dev-dashboard.ts +4 -4
- package/src/dev-lock.ts +18 -2
- package/src/dev-port.ts +21 -0
- package/src/dev-route-table.ts +3 -3
- package/src/dispatch.ts +12 -5
- package/src/document-styles.ts +1 -1
- package/src/drift.ts +4 -18
- package/src/error-catalog.ts +16 -9
- package/src/error-codes.ts +28 -30
- package/src/error-pages.ts +19 -7
- package/src/errors.ts +14 -0
- package/src/favicon.ts +2 -2
- package/src/fix-imports.ts +1 -1
- package/src/fix-scan.ts +2 -9
- package/src/flag-reads.ts +1 -2
- package/src/foreign-text.ts +36 -0
- package/src/framework-schema.ts +5 -6
- package/src/generate-feature.ts +42 -0
- package/src/generate-files.ts +7 -3
- package/src/generate-grants.ts +83 -0
- package/src/generate-kinds.ts +56 -4
- package/src/i18n-index.ts +59 -10
- package/src/icon-assets.ts +1 -1
- package/src/image-prepare.ts +14 -0
- package/src/import-scan.ts +63 -0
- package/src/index.ts +67 -348
- package/src/invocation-flags.ts +26 -0
- package/src/island-bundle.ts +6 -1
- package/src/island-capture.ts +6 -6
- package/src/island-harness-script.ts +6 -2
- package/src/island-realtime.ts +15 -8
- package/src/island-shot.ts +5 -0
- package/src/island-store.ts +131 -0
- package/src/island-verdict.ts +1 -1
- package/src/job-registration.ts +42 -0
- package/src/jobs-driver.ts +2 -2
- package/src/load-findings.ts +51 -0
- package/src/mcp-db-target.ts +1 -1
- package/src/mcp-errors.ts +28 -27
- package/src/mcp-host.ts +27 -12
- package/src/mcp-ui-diff.ts +27 -0
- package/src/mcp-ui-inspect.ts +4 -4
- package/src/mcp-ui-interact.ts +21 -15
- package/src/mcp-ui.ts +23 -17
- package/src/measure-database.ts +73 -0
- package/src/measure-paths.ts +74 -0
- package/src/measure-scope.ts +74 -0
- package/src/messages.ts +1 -3
- package/src/metrics-endpoint.ts +1 -1
- package/src/otlp-export.ts +1 -1
- package/src/output.ts +6 -0
- package/src/permission-grants.ts +86 -0
- package/src/prerender-out.ts +25 -0
- package/src/prerender.ts +121 -102
- package/src/pwa-artifacts.ts +3 -3
- package/src/reexport-manifest.ts +2 -1
- package/src/registry.ts +80 -56
- package/src/role-realtime.ts +36 -0
- package/src/{dev-replicator.ts → role-replicator.ts} +1 -1
- package/src/{dev-roles-fixture.ts → role-start-fixture.ts} +9 -5
- package/src/role-start-types.ts +112 -0
- package/src/{dev-roles.ts → role-start.ts} +42 -115
- package/src/{dev-sync.ts → role-sync.ts} +22 -8
- package/src/root-env.ts +67 -0
- package/src/{dev-assets.ts → runtime-assets.ts} +7 -7
- package/src/{dev-services.ts → runtime-bindings.ts} +39 -16
- package/src/{dev-cache.ts → runtime-cache.ts} +2 -2
- package/src/runtime-jobs.ts +87 -0
- package/src/{dev-live-feed.ts → runtime-live-feed.ts} +18 -5
- package/src/{dev-notify-retention.ts → runtime-notify-retention.ts} +1 -1
- package/src/{dev-purge.ts → runtime-purge.ts} +2 -2
- package/src/{dev-queue.ts → runtime-queue.ts} +4 -4
- package/src/runtime-realtime.ts +55 -0
- package/src/{dev-replica.ts → runtime-replica.ts} +2 -2
- package/src/{dev-runtime.ts → runtime-services.ts} +41 -17
- package/src/{dev-storage.ts → runtime-storage.ts} +4 -4
- package/src/scaffold-fixture.ts +28 -6
- package/src/scaffold-typecheck.ts +6 -3
- package/src/schema-drift.ts +7 -1
- package/src/script-csp.ts +5 -2
- package/src/secrets-rotation.ts +59 -0
- package/src/serve-boot.ts +192 -0
- package/src/serve-drain.ts +24 -0
- package/src/serve-entry.ts +6 -0
- package/src/serve-env.ts +116 -0
- package/src/serve-types.ts +55 -0
- package/src/serve.ts +44 -347
- package/src/shot-server.ts +2 -2
- package/src/shot-settle.ts +10 -1
- package/src/shot-theme.ts +3 -3
- package/src/shot-verdict.ts +16 -7
- package/src/signal-shred.ts +27 -0
- package/src/solid-loader.ts +26 -2
- package/src/static-report.ts +8 -1
- package/src/sync-url.ts +21 -1
- package/src/templates/action.ts +30 -16
- package/src/templates/entity.ts +12 -7
- package/src/templates/index.ts +1 -1
- package/src/templates/job.ts +10 -7
- package/src/templates/policy.ts +20 -2
- package/src/templates/resource-create.ts +127 -0
- package/src/templates/resource-form-island.ts +49 -12
- package/src/templates/resource.ts +10 -4
- package/src/templates/scaffold-app.ts +4 -1
- package/src/templates/scaffold-auth.ts +3 -1
- package/src/templates/scaffold-container-compose.ts +184 -0
- package/src/templates/scaffold-container.ts +24 -159
- package/src/templates/scaffold-dashboard-example.ts +2 -2
- package/src/templates/scaffold-db-package.ts +16 -3
- package/src/templates/scaffold-demo-org.ts +41 -0
- package/src/templates/scaffold-entries.ts +1 -1
- package/src/templates/scaffold-env.ts +6 -0
- package/src/templates/scaffold-helm-templates.ts +66 -7
- package/src/templates/scaffold-helm.ts +27 -5
- package/src/templates/scaffold-i18n.ts +15 -10
- package/src/templates/scaffold-repo.ts +13 -10
- package/src/templates/scaffold-roles.ts +38 -10
- package/src/templates/slice-foundation.ts +1 -1
- package/src/templates/wrap.ts +4 -1
- package/src/test-passes.ts +2 -1
- package/src/test-workers.ts +26 -0
- package/src/ts-scan.ts +3 -6
- package/src/tsconfig-references.ts +1 -2
- package/src/verify-checks.ts +41 -39
- package/src/verify-e2e.ts +10 -7
- package/src/verify-run.ts +11 -1
- package/src/verify-step.ts +3 -3
- package/src/verify-tests.ts +1 -26
- package/src/verify-typecheck.ts +28 -0
- package/src/web-binding.ts +2 -2
- package/src/workspace-graph.ts +10 -33
- package/src/cdp-browser.ts +0 -94
- package/src/cdp-connection.ts +0 -247
- package/src/cdp-e2e-page.ts +0 -180
- package/src/cdp-e2e-session.ts +0 -199
- package/src/cdp-errors.ts +0 -56
- package/src/cdp-launch.ts +0 -193
- package/src/cdp-offline-script.ts +0 -73
- package/src/cdp-pipe.ts +0 -77
- package/src/e2e-app.ts +0 -103
- package/src/e2e-browser-handle.ts +0 -55
- package/src/e2e-dom-fixture.ts +0 -117
- package/src/e2e-driver.ts +0 -117
- package/src/e2e-errors.ts +0 -117
- package/src/e2e-evaluate.ts +0 -156
- package/src/e2e-locator.ts +0 -86
- package/src/e2e-page.ts +0 -153
- package/src/e2e-preload.ts +0 -64
- package/src/e2e-probe.ts +0 -23
- package/src/e2e-selection.ts +0 -182
- package/src/e2e-spawn.ts +0 -169
- package/src/measurement-actor.ts +0 -26
- /package/src/{dev-hooks.ts → runtime-hooks.ts} +0 -0
- /package/src/{dev-render.ts → runtime-render.ts} +0 -0
package/src/verify-checks.ts
CHANGED
|
@@ -15,12 +15,13 @@ import {
|
|
|
15
15
|
import type { MetaIssue } from '@ultimat3/seo';
|
|
16
16
|
import { validateMeta } from '@ultimat3/seo';
|
|
17
17
|
import { checkAgentsMd } from './app-agents-md';
|
|
18
|
-
import { checkAppBoundaries } from './app-boundaries';
|
|
19
18
|
import { envExampleFindings } from './app-env';
|
|
20
19
|
import { appManifest, readAppManifest } from './app-manifest';
|
|
21
|
-
import { OPENAPI_FILE,
|
|
20
|
+
import { OPENAPI_FILE, openApiStaleness } from './app-openapi';
|
|
22
21
|
import { policyFindings } from './app-permissions';
|
|
23
22
|
import { APP_CONFIG_FILE } from './app-root';
|
|
23
|
+
import { asyncPageFindings } from './async-pages';
|
|
24
|
+
import { appBoundaryFindings } from './boundary-findings';
|
|
24
25
|
import { checkBudgets, readBuildStats } from './budgets';
|
|
25
26
|
import { checkDestructiveMigrations } from './db-destructive';
|
|
26
27
|
import { checkUngeneratableMigrations } from './db-ungeneratable';
|
|
@@ -28,17 +29,21 @@ import { checkDocumentStyles, documentSurfaces } from './document-styles';
|
|
|
28
29
|
import { checkErrorCodeResolution, checkErrorFixReport } from './error-contract';
|
|
29
30
|
import { guardFindings } from './guards';
|
|
30
31
|
import { catalogFindings } from './i18n-registration';
|
|
32
|
+
import { unregisteredJobFindings } from './job-registration';
|
|
31
33
|
import { liveRouteFindings } from './live-routes';
|
|
34
|
+
import { withLoadFindings, withoutLoadFindings } from './load-findings';
|
|
32
35
|
import { msg } from './messages';
|
|
33
36
|
import type { Finding } from './output';
|
|
34
37
|
import { findingFrom } from './output';
|
|
35
38
|
import { checkMigrationDrift } from './schema-drift';
|
|
36
39
|
import { scanSiteMeta } from './seo-meta';
|
|
40
|
+
import { quoteArg } from './shell-quote';
|
|
37
41
|
import { readStaticReport } from './static-report';
|
|
38
42
|
import { floorProblemFindings, readVerifyFloor } from './verify-floor';
|
|
39
43
|
import type { VerifyStep } from './verify-step';
|
|
40
44
|
import { fromExec, fromFindings, hostFindings } from './verify-step';
|
|
41
45
|
import { TEST_STEPS } from './verify-tests';
|
|
46
|
+
import { typecheckArgs } from './verify-typecheck';
|
|
42
47
|
import { checkFileSizes, checkPackageShape, hasWorkspacePackages } from './workspace-checks';
|
|
43
48
|
import { checkWorkspaceDependencies } from './workspace-graph';
|
|
44
49
|
|
|
@@ -49,22 +54,21 @@ const ROADMAP_FILE = join('docs', 'idea', '14-roadmap.md');
|
|
|
49
54
|
export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
50
55
|
{
|
|
51
56
|
name: 'typecheck',
|
|
52
|
-
summary: 'tsc -b across every project the root references',
|
|
57
|
+
summary: 'tsc -b across every project the root references, tsc -p . where it references none',
|
|
53
58
|
// `typecheckBin` (`x.verify.json`, beside `agentsMdMaxBytes`) swaps the binary and nothing
|
|
54
|
-
// else: same
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
59
|
+
// else: same invocation, same output format to parse, same `X_TYPECHECK_FAILED` finding either
|
|
60
|
+
// way. Absent means `tsc` — the only checker every app already has, since `typescript` is a
|
|
61
|
+
// framework dependency and a drop-in replacement is the app's own devDependency to add, never
|
|
62
|
+
// a default this step could assume. `-b` or `-p .` is `typecheckArgs`' call (#450).
|
|
58
63
|
async run(ctx) {
|
|
59
64
|
const floor = await readVerifyFloor(ctx.root);
|
|
60
|
-
const
|
|
61
|
-
const result = await ctx.runner(['bunx',
|
|
62
|
-
cwd: ctx.root,
|
|
63
|
-
});
|
|
65
|
+
const argv = await typecheckArgs(ctx.root, floor?.typecheckBin ?? 'tsc');
|
|
66
|
+
const result = await ctx.runner(['bunx', ...argv], { cwd: ctx.root });
|
|
64
67
|
return fromExec(result, {
|
|
65
68
|
code: 'X_TYPECHECK_FAILED',
|
|
66
69
|
cause: 'the project does not typecheck',
|
|
67
|
-
|
|
70
|
+
// Joined, never spliced: `typecheckBin` is a value from the app's `x.verify.json`.
|
|
71
|
+
fix: ['bunx', ...argv.map(quoteArg)].join(' '),
|
|
68
72
|
});
|
|
69
73
|
},
|
|
70
74
|
},
|
|
@@ -99,7 +103,7 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
99
103
|
// extension model rejects.
|
|
100
104
|
run: async (ctx) =>
|
|
101
105
|
fromFindings([
|
|
102
|
-
...(await
|
|
106
|
+
...(await appBoundaryFindings(ctx.root)),
|
|
103
107
|
...(await guardFindings(ctx.root)),
|
|
104
108
|
...(await hostFindings(ctx, 'boundaries')),
|
|
105
109
|
]),
|
|
@@ -187,10 +191,11 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
187
191
|
async run(ctx) {
|
|
188
192
|
const committed = await readAppManifest(ctx.root);
|
|
189
193
|
const { manifest, findings } = await appManifest(ctx.root);
|
|
190
|
-
|
|
194
|
+
// A module that will not import is `manifest`'s to report (`load-findings.ts`).
|
|
195
|
+
return withoutLoadFindings(ctx.root, [
|
|
191
196
|
...findings,
|
|
192
197
|
...(committed === undefined ? [] : contractFindings(committed, manifest)),
|
|
193
|
-
...(await
|
|
198
|
+
...(await openApiStaleness(ctx.root, manifest)),
|
|
194
199
|
]);
|
|
195
200
|
},
|
|
196
201
|
},
|
|
@@ -224,21 +229,27 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
224
229
|
// reported under `X_ISLAND_PROPS_INVALID` — the build's own sentence, naming the prop and
|
|
225
230
|
// its bytes — and not as an `X_BUDGET_UNMEASURED` whose fix is to go and read this file.
|
|
226
231
|
const report = await readStaticReport(ctx.root);
|
|
227
|
-
// The load's own findings
|
|
228
|
-
//
|
|
229
|
-
// `X_BUDGET_UNMEASURED` — the symptom
|
|
230
|
-
// not compile. `contract-diff` reports these too when it applies; two red steps naming one
|
|
231
|
-
// broken module is honest, and one of them silently green over it is the false green.
|
|
232
|
+
// The load's own findings are `manifest`'s, reported there once; this step's output says so.
|
|
233
|
+
// A module that would not import registers no route, so every route it declared still reads
|
|
234
|
+
// as `X_BUDGET_UNMEASURED` here — the symptom stays this step's, and the gate stays red.
|
|
232
235
|
const { manifest, findings } = await appManifest(ctx.root);
|
|
233
|
-
|
|
236
|
+
const outcome = await withoutLoadFindings(ctx.root, [
|
|
234
237
|
...findings,
|
|
235
238
|
...checkDocumentStyles(documentSurfaces()),
|
|
236
239
|
// The third rider, and the same question this step already asks one level down: what
|
|
237
240
|
// JavaScript does this route's document boot? A live read with no island is a route
|
|
238
241
|
// whose answer is "none", which no suite can fail on — the page renders, at 200.
|
|
239
242
|
...(await liveRouteFindings(ctx.root)),
|
|
243
|
+
// The fourth: a data read in an async Page rather than in `load` (`async-pages.ts`).
|
|
244
|
+
...asyncPageFindings(),
|
|
240
245
|
...checkBudgets(manifest, stats, report?.unmeasured),
|
|
241
246
|
]);
|
|
247
|
+
// What no build can weigh, said out loud rather than dropped (`measure-paths.ts`).
|
|
248
|
+
const notes = (report?.unmeasured ?? [])
|
|
249
|
+
.filter((one) => one.weighable === false)
|
|
250
|
+
.map((one) => `not weighed: ${one.reason}`);
|
|
251
|
+
const output = [outcome.output, ...notes].filter((line) => line !== undefined).join('\n');
|
|
252
|
+
return output === '' ? outcome : { ...outcome, output };
|
|
242
253
|
},
|
|
243
254
|
},
|
|
244
255
|
{
|
|
@@ -293,7 +304,7 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
293
304
|
// registers no routes — SKIPPED there, never passed, for the reason `i18n` gives: a step that
|
|
294
305
|
// answers `ok` about nothing is the vacuous green these checks exist to refuse.
|
|
295
306
|
applies: async (ctx) => existsSync(join(ctx.root, APP_CONFIG_FILE)),
|
|
296
|
-
run: async (ctx) =>
|
|
307
|
+
run: async (ctx) => withoutLoadFindings(ctx.root, await policyFindings(ctx.root)),
|
|
297
308
|
},
|
|
298
309
|
{
|
|
299
310
|
name: 'manifest',
|
|
@@ -315,9 +326,16 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
315
326
|
// and reading it after the check would enforce the default on a repo that raised it.
|
|
316
327
|
const floor = await readVerifyFloor(ctx.root);
|
|
317
328
|
const agents = await checkAgentsMd(ctx.root, floor?.agentsMdMaxBytes);
|
|
329
|
+
// The app's load failures, once, here — every other step that loaded it points at this one.
|
|
330
|
+
const app = existsSync(join(ctx.root, APP_CONFIG_FILE));
|
|
331
|
+
const drift = await driftFindings(ctx.root);
|
|
318
332
|
const findings = [
|
|
319
333
|
...manifestMissingFindings(ctx.root),
|
|
320
|
-
...(await
|
|
334
|
+
...(app ? await withLoadFindings(ctx.root, drift) : drift),
|
|
335
|
+
// An app only: the framework monorepo declares no `defineApi`, so it has no list to be in.
|
|
336
|
+
...(existsSync(join(ctx.root, APP_CONFIG_FILE))
|
|
337
|
+
? await unregisteredJobFindings(ctx.root)
|
|
338
|
+
: []),
|
|
321
339
|
...(await envExampleFindings(ctx.root)),
|
|
322
340
|
...floorProblemFindings(floor),
|
|
323
341
|
...agents.findings,
|
|
@@ -394,22 +412,6 @@ function contractFindings(before: Manifest, after: Manifest): readonly Finding[]
|
|
|
394
412
|
}
|
|
395
413
|
}
|
|
396
414
|
|
|
397
|
-
/** The typed client is generated from `openapi.json`, so a stale spec ships a wrong client. */
|
|
398
|
-
async function specFindings(root: string, manifest: Manifest): Promise<readonly Finding[]> {
|
|
399
|
-
const path = join(root, OPENAPI_FILE);
|
|
400
|
-
if (!existsSync(path)) return [];
|
|
401
|
-
if ((await Bun.file(path).text()) === openApiJson(manifest)) return [];
|
|
402
|
-
return [
|
|
403
|
-
{
|
|
404
|
-
code: 'X_MANIFEST_STALE',
|
|
405
|
-
cause: `${OPENAPI_FILE} does not match the actions the code registers`,
|
|
406
|
-
fix: 'x manifest',
|
|
407
|
-
docs: ERROR_DOCS_URL,
|
|
408
|
-
at: OPENAPI_FILE,
|
|
409
|
-
},
|
|
410
|
-
];
|
|
411
|
-
}
|
|
412
|
-
|
|
413
415
|
/**
|
|
414
416
|
* One `MetaIssue` as the gate reports it. `at` is the route FILE and never the URL: every seo error
|
|
415
417
|
* already names the file in its cause, and `at` is what an agent opens.
|
package/src/verify-e2e.ts
CHANGED
|
@@ -4,14 +4,17 @@
|
|
|
4
4
|
// app: the suite runs as before and its browser-backed cases skip — or refuse under
|
|
5
5
|
// `E2E_BROWSER_REQUIRED=1`.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
import { join } from 'node:path';
|
|
9
|
-
import { findChrome } from './cdp-launch';
|
|
10
|
-
import { E2E_ROOT_ENV } from './e2e-browser-handle';
|
|
7
|
+
import { E2E_ROOT_ENV, findChrome } from '@ultimat3/testing';
|
|
11
8
|
import type { ExecResult } from './exec';
|
|
12
9
|
|
|
13
|
-
/**
|
|
14
|
-
|
|
10
|
+
/**
|
|
11
|
+
* The preload `bun test` is handed, beside the app's own from `bunfig.toml` — `@ultimat3/testing`'s,
|
|
12
|
+
* resolved from here to the absolute path the child process loads. A FUNCTION, resolved when a run
|
|
13
|
+
* needs it: at module scope it ran on every import of the registry, and a compiled `x` binary —
|
|
14
|
+
* whose `/$bunfs` holds no `node_modules` — died at boot resolving a file it never uses.
|
|
15
|
+
*/
|
|
16
|
+
export const e2ePreload = (): string =>
|
|
17
|
+
Bun.resolveSync('@ultimat3/testing/e2e-preload', import.meta.dir);
|
|
15
18
|
|
|
16
19
|
export interface E2eRun {
|
|
17
20
|
readonly command: readonly string[];
|
|
@@ -32,7 +35,7 @@ export async function withE2eApp(
|
|
|
32
35
|
if (chrome === undefined) return run({ command: input.command, env: input.env });
|
|
33
36
|
const [bun = 'bun', test = 'test', ...rest] = input.command;
|
|
34
37
|
return run({
|
|
35
|
-
command: [bun, test, '--preload',
|
|
38
|
+
command: [bun, test, '--preload', e2ePreload(), ...rest],
|
|
36
39
|
env: { ...input.env, [E2E_ROOT_ENV]: input.root },
|
|
37
40
|
});
|
|
38
41
|
}
|
package/src/verify-run.ts
CHANGED
|
@@ -128,6 +128,8 @@ function verifySummary(input: {
|
|
|
128
128
|
* files; the rest are in-process scans), so they run BESIDE the serial suites — `live` and `e2e`
|
|
129
129
|
* are one worker each, Postgres- and browser-bound, and mostly waiting. `typecheck` stays first
|
|
130
130
|
* and alone: `tsc -b` writes `.tsbuildinfo` and `dist/`, and `unit` saturates every core.
|
|
131
|
+
* `manifest` joined them 2026-09-23: it compares committed files against the code and writes
|
|
132
|
+
* nothing, and at the repo root its host checks are ~20 whole-tree reads that sat alone at the end.
|
|
131
133
|
*/
|
|
132
134
|
export const BESIDE_SERIAL_SUITES: ReadonlySet<string> = new Set([
|
|
133
135
|
'lint',
|
|
@@ -135,6 +137,7 @@ export const BESIDE_SERIAL_SUITES: ReadonlySet<string> = new Set([
|
|
|
135
137
|
'filesize',
|
|
136
138
|
'package-shape',
|
|
137
139
|
'errors',
|
|
140
|
+
'manifest',
|
|
138
141
|
]);
|
|
139
142
|
|
|
140
143
|
/** The consecutive run of steps the static group overlaps, in the order `VERIFY_STEP_NAMES` holds. */
|
|
@@ -145,7 +148,14 @@ async function runStep(
|
|
|
145
148
|
ctx: VerifyContext,
|
|
146
149
|
floor: Awaited<ReturnType<typeof readVerifyFloor>>,
|
|
147
150
|
): Promise<StepResult> {
|
|
148
|
-
|
|
151
|
+
// Inside the step's own failure, like a throwing `run`: an `applies` that threw escaped every
|
|
152
|
+
// catch and aborted the whole gate, which is the one outcome `runVerify` exists to prevent.
|
|
153
|
+
let applies: boolean;
|
|
154
|
+
try {
|
|
155
|
+
applies = step.applies === undefined ? true : await step.applies(ctx);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
return { name: step.name, ok: false, durationMs: 0, findings: [findingOf(error, step.name)] };
|
|
158
|
+
}
|
|
149
159
|
if (!applies) {
|
|
150
160
|
// A skip this repo already ruled out is not a skip. The step ran here before — the floor is
|
|
151
161
|
// that claim, committed — so "nothing to check" now means the suite was deleted, and the
|
package/src/verify-step.ts
CHANGED
|
@@ -79,9 +79,9 @@ export interface VerifyContext {
|
|
|
79
79
|
*/
|
|
80
80
|
readonly workers?: number;
|
|
81
81
|
/**
|
|
82
|
-
* ONE step, by name — an iteration loop, and the one thing here that IS a narrowing.
|
|
83
|
-
*
|
|
84
|
-
* question about one step. It does not weaken axiom 5, and the two rules that keep it honest are
|
|
82
|
+
* ONE step, by name — an iteration loop, and the one thing here that IS a narrowing. The
|
|
83
|
+
* whole gate costs minutes (3m19s at the framework root on 12 cores, `As of 2026-09-23`), which
|
|
84
|
+
* is the cost of asking a question about one step. It does not weaken axiom 5, and the two rules that keep it honest are
|
|
85
85
|
* mechanical rather than remembered: a run with this set prints `NOT A GATE RUN` in the summary
|
|
86
86
|
* AND carries `notAGateRun` in `--json` (`verify-run.ts`), so no reader of either can mistake it
|
|
87
87
|
* for the gate; and nothing writes `x.verify.json`, so the suite floor cannot be lowered by a
|
package/src/verify-tests.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { countsOf } from './test-counts';
|
|
|
18
18
|
import { testEnvOverrides } from './test-dotenv';
|
|
19
19
|
import type { TestFile } from './test-select';
|
|
20
20
|
import { discoverTests } from './test-select';
|
|
21
|
-
import { defaultWorkers } from './test-workers';
|
|
21
|
+
import { defaultWorkers, SERIAL_TYPES } from './test-workers';
|
|
22
22
|
import { withE2eApp } from './verify-e2e';
|
|
23
23
|
import type { StepOutcome, VerifyContext, VerifyStep } from './verify-step';
|
|
24
24
|
import { fromExec, fromFindings } from './verify-step';
|
|
@@ -111,31 +111,6 @@ export const ownerOf = (path: string): TestType =>
|
|
|
111
111
|
const disownedBy = (type: TypedTest): readonly string[] =>
|
|
112
112
|
type === 'e2e' ? [`**/e2e/**/*.{${OUTRANKING_E2E.join(',')}}.test.*`] : [];
|
|
113
113
|
|
|
114
|
-
/**
|
|
115
|
-
* Which types run across worker processes, and why the other two cannot.
|
|
116
|
-
*
|
|
117
|
-
* Parallel is safe when the only thing a test file shares with another file is the database, and
|
|
118
|
-
* the database is per worker by construction (`ULTIMATE_TEST_WORKER` → one clone of the migrated
|
|
119
|
-
* template, `@ultimat3/testing`'s `acquireWorkerDatabase`). Every other process-global in this
|
|
120
|
-
* framework — the permission set, the roles, the entity/action/query registries, the error-code
|
|
121
|
-
* titles, the fixture bag — is handled by `--isolate` giving each FILE its own module registry.
|
|
122
|
-
*
|
|
123
|
-
* | Type | Why |
|
|
124
|
-
* |---|---|
|
|
125
|
-
* | `live` | **serial.** A logical replication slot and a publication are named at the Postgres
|
|
126
|
-
* CLUSTER level, not inside a database, and this repo's own feed tests hard-code
|
|
127
|
-
* `x_live_slot` / `x_live_pub` against `TEST_REPLICATION_URL` — the one server, never a per-worker
|
|
128
|
-
* clone. Two workers would race `pg_create_logical_replication_slot` and the loser's failure would
|
|
129
|
-
* read as a flake. A per-worker database does not isolate a cluster-wide object |
|
|
130
|
-
* | `e2e` | **serial.** It runs against the *built output*: one `dist/`, one service-worker
|
|
131
|
-
* registration, one browser profile. There is nothing per-worker to hand it, and the type is
|
|
132
|
-
* seconds at most, so a split would buy a race and no time |
|
|
133
|
-
*
|
|
134
|
-
* The two are named here rather than tested for, because "can this type be sharded?" is a design
|
|
135
|
-
* fact about the type, not something a run can discover about itself.
|
|
136
|
-
*/
|
|
137
|
-
export const SERIAL_TYPES: readonly TestType[] = ['live', 'e2e'];
|
|
138
|
-
|
|
139
114
|
const isSerial = (type: TestType): boolean => SERIAL_TYPES.includes(type);
|
|
140
115
|
|
|
141
116
|
/**
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Which `tsc` invocation the `typecheck` step runs. `tsc -b` decides "up to date" by mtime, and Bun
|
|
2
|
+
// installs a package as HARDLINKS out of its cache, carrying the cache's old mtimes — so upgrading
|
|
3
|
+
// a dependency to a version already in the cache left every input older than the buildinfo, `-b`
|
|
4
|
+
// skipped the program and reported green while a cold CI went red (#450). `-p` with `incremental`
|
|
5
|
+
// re-reads every input and compares content hashes, so it cannot be fooled by a date.
|
|
6
|
+
|
|
7
|
+
import { checkRootReferences } from './tsconfig-references';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A package dir no reference can name. `checkRootReferences` answers `[]` when the root has no
|
|
11
|
+
* `references` array at all and one finding for this probe when it has one, which is the one
|
|
12
|
+
* question asked here — without a second JSONC parser beside the one that file owns.
|
|
13
|
+
*/
|
|
14
|
+
const PROBE = '\u0000x-verify-typecheck-probe';
|
|
15
|
+
|
|
16
|
+
/** True when the root tsconfig declares `references`: a build graph that only `-b` can walk. */
|
|
17
|
+
export const usesProjectReferences = async (root: string): Promise<boolean> =>
|
|
18
|
+
(await checkRootReferences(root, [PROBE])).length > 0;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The argv after `bunx`. A root with references keeps `-b` — the framework's 32 projects are only
|
|
22
|
+
* reachable that way, and `tsc -b` rebuilds a project whose own sources moved. A root without them
|
|
23
|
+
* (every scaffolded app) is ONE program, and `-p .` is the content-hashed check of it.
|
|
24
|
+
*/
|
|
25
|
+
export async function typecheckArgs(root: string, bin: string): Promise<readonly string[]> {
|
|
26
|
+
const mode = (await usesProjectReferences(root)) ? ['-b'] : ['-p', '.'];
|
|
27
|
+
return [bin, ...mode, '--pretty', 'false'];
|
|
28
|
+
}
|
package/src/web-binding.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// How a process binds its sockets, and what it admits about itself. A LEAF: it imports nothing,
|
|
2
|
-
// so every role can read it without pulling `
|
|
3
|
-
// `
|
|
2
|
+
// so every role can read it without pulling `role-start` — which is what made this its own file.
|
|
3
|
+
// `role-sync` needs the default and `role-start` already imports `role-sync`, so reading it from
|
|
4
4
|
// there would be a runtime import cycle in the framework's own boot path.
|
|
5
5
|
|
|
6
6
|
export interface WebBinding {
|
package/src/workspace-graph.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
8
|
+
import { scanAllImports } from './import-scan';
|
|
8
9
|
import type { Finding } from './output';
|
|
9
10
|
import { eachSourceFile, isGenerated, isTest, isVendored } from './source-files';
|
|
10
|
-
import { maskLiterals } from './ts-scan';
|
|
11
11
|
|
|
12
12
|
export interface WorkspaceNode {
|
|
13
13
|
/** package.json "name". */
|
|
@@ -144,43 +144,20 @@ export function packageOfSpecifier(specifier: string): string | undefined {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
/**
|
|
147
|
-
* Every
|
|
148
|
-
*
|
|
149
|
-
*
|
|
147
|
+
* Every package a source file imports, deduplicated, in first-appearance order — type-only imports
|
|
148
|
+
* included, because a type edge still needs its manifest line. The transpiler is the parser, so a
|
|
149
|
+
* generator's template literal (even one nested in a `${…}`, #493) is a string and never an import,
|
|
150
|
+
* and `from<Row>('posts')` is a call. A file it cannot parse throws rather than reading as empty.
|
|
150
151
|
*/
|
|
151
|
-
|
|
152
|
-
/(?:\bfrom\s*|\bimport\s*\(\s*|\brequire\s*\(\s*|(?:^|[;{])\s*import\s+)['"]([^'"]*)['"]/g;
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Every package a source file imports, deduplicated, in first-appearance order.
|
|
156
|
-
*
|
|
157
|
-
* Read from the MASKED text, because a generator's template literal holds whole programs: every
|
|
158
|
-
* `templates/*.ts` here emits `import … from '@ultimat3/ui'` as a string, and a scan that read
|
|
159
|
-
* those would bill the CLI for the imports of the app it writes. The line a masked hit falls on is
|
|
160
|
-
* then re-read from the real source — the specifier itself is what masking blanks.
|
|
161
|
-
*/
|
|
162
|
-
export function importedPackages(source: string): readonly string[] {
|
|
163
|
-
const masked = maskLiterals(source).split('\n');
|
|
164
|
-
const lines = source.split('\n');
|
|
152
|
+
export function importedPackages(source: string, path = 'source.ts'): readonly string[] {
|
|
165
153
|
const packages = new Set<string>();
|
|
166
|
-
for (const
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
if (line === undefined || [...maskedLine.matchAll(IMPORT_FORM)].length === 0) continue;
|
|
170
|
-
for (const match of line.matchAll(IMPORT_FORM)) {
|
|
171
|
-
const name = packageOfSpecifier(match[1] ?? '');
|
|
172
|
-
if (name !== undefined) packages.add(name);
|
|
173
|
-
}
|
|
154
|
+
for (const specifier of scanAllImports({ path, source })) {
|
|
155
|
+
const name = packageOfSpecifier(specifier);
|
|
156
|
+
if (name !== undefined) packages.add(name);
|
|
174
157
|
}
|
|
175
158
|
return [...packages];
|
|
176
159
|
}
|
|
177
160
|
|
|
178
|
-
/**
|
|
179
|
-
* Borrowed, not twinned: `X_APP_PACKAGE_INVALID` already means "this package.json supplies no
|
|
180
|
-
* usable name", and a workspace manifest is that same file one directory down. `bun pm pkg set`
|
|
181
|
-
* is not the fix here — it parses the file it edits, so it fails on exactly the input this
|
|
182
|
-
* reports.
|
|
183
|
-
*/
|
|
184
161
|
export const unreadableWorkspaceFinding = (path: string): Finding => ({
|
|
185
162
|
code: 'X_APP_PACKAGE_INVALID',
|
|
186
163
|
cause: `${path} is claimed by the root "workspaces" globs and supplies no readable "name"`,
|
|
@@ -228,7 +205,7 @@ export async function checkWorkspaceDependencies(root: string): Promise<readonly
|
|
|
228
205
|
if (isTest(path) || isGenerated(path)) continue;
|
|
229
206
|
const owner = owners.find((node) => path.startsWith(`${node.dir}/`));
|
|
230
207
|
if (owner === undefined) continue;
|
|
231
|
-
for (const name of importedPackages(await Bun.file(join(root, path)).text())) {
|
|
208
|
+
for (const name of importedPackages(await Bun.file(join(root, path)).text(), path)) {
|
|
232
209
|
const target = byName.get(name);
|
|
233
210
|
if (target === undefined || target.name === owner.name) continue;
|
|
234
211
|
if (owner.dependencies.includes(target.name)) continue;
|
package/src/cdp-browser.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
// One responsibility: compose the halves — find a browser, launch it over its pipe, attach a page —
|
|
2
|
-
// into the one object `installE2eDriver({ page })` takes, plus the way to shut it down.
|
|
3
|
-
//
|
|
4
|
-
// **Absent is a SKIP, never a failure, and that is a requirement rather than a state.** A CI box
|
|
5
|
-
// with no Chrome must not turn the `e2e` step red for a reason unrelated to the change, which is
|
|
6
|
-
// the rule `packages/cli/CLAUDE.md` already states about `x shot`, `x pr` and `x ci`.
|
|
7
|
-
// `openE2eBrowserIfAvailable` is that door; `openE2eBrowser` refuses by name for a caller that has
|
|
8
|
-
// already decided a browser is required.
|
|
9
|
-
|
|
10
|
-
import { finiteCount } from '@ultimat3/core';
|
|
11
|
-
import type { E2eTab } from './cdp-e2e-page';
|
|
12
|
-
import type { E2eSession } from './cdp-e2e-session';
|
|
13
|
-
import { cdpE2eSession } from './cdp-e2e-session';
|
|
14
|
-
import { CdpBrowserMissingError } from './cdp-errors';
|
|
15
|
-
import type { LaunchedBrowser } from './cdp-launch';
|
|
16
|
-
import { CHROME_CANDIDATES, findChrome, launchChrome } from './cdp-launch';
|
|
17
|
-
|
|
18
|
-
/** How long a launch, a connect or a single CDP call may take. One number, three deadlines. */
|
|
19
|
-
export const DEFAULT_CDP_TIMEOUT_MS = 30_000;
|
|
20
|
-
|
|
21
|
-
export interface E2eBrowser {
|
|
22
|
-
/** The first tab — what `installE2eDriver({ page })` drives. */
|
|
23
|
-
readonly page: E2eTab;
|
|
24
|
-
/**
|
|
25
|
-
* The browser itself: more tabs in the same profile, init scripts, the offline switch for every
|
|
26
|
-
* page and worker, and the log of every socket and request. What a multi-tab acceptance suite
|
|
27
|
-
* drives, on the same launch as `page` — one harness, never a second one beside the driver.
|
|
28
|
-
*/
|
|
29
|
-
readonly session: E2eSession;
|
|
30
|
-
/** Idempotent, and it closes both halves: the CDP socket, then the process and its profile. */
|
|
31
|
-
close(): void;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface OpenE2eBrowserOptions {
|
|
35
|
-
readonly env?: Readonly<Record<string, string | undefined>> | undefined;
|
|
36
|
-
readonly timeoutMs?: number | undefined;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Screened HERE, before a browser exists, and not where it lands. It becomes three deadlines — the
|
|
41
|
-
* launch, the handshake and every CDP call — and `Number(process.env.E2E_TIMEOUT ?? '')` is `NaN`
|
|
42
|
-
* for an unset variable and is not nullish, so `??` keeps it: a `setTimeout` given `NaN` fires at
|
|
43
|
-
* 1ms in this Bun, which makes every call report `X_CDP_TIMEOUT` against a browser that was
|
|
44
|
-
* answering. A misdiagnosis reported as a test failure is worse than the failure.
|
|
45
|
-
*/
|
|
46
|
-
const budget = (options: OpenE2eBrowserOptions): number =>
|
|
47
|
-
finiteCount('openE2eBrowser', 'timeoutMs', options.timeoutMs ?? DEFAULT_CDP_TIMEOUT_MS);
|
|
48
|
-
|
|
49
|
-
const compose = (launched: LaunchedBrowser, session: E2eSession, page: E2eTab): E2eBrowser => ({
|
|
50
|
-
page,
|
|
51
|
-
session,
|
|
52
|
-
// The connection first, then the process — `launched.close()` does both, in that order: closing
|
|
53
|
-
// the process out from under an open connection makes every in-flight call report "the browser
|
|
54
|
-
// closed the CDP connection", which is true and useless.
|
|
55
|
-
close: () => launched.close(),
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Launch a browser and attach one page to it. Refuses with `X_CDP_BROWSER_MISSING` when there is
|
|
60
|
-
* nothing to launch — the caller that wants a skip asks `openE2eBrowserIfAvailable` instead.
|
|
61
|
-
*/
|
|
62
|
-
export async function openE2eBrowser(options: OpenE2eBrowserOptions = {}): Promise<E2eBrowser> {
|
|
63
|
-
const timeoutMs = budget(options);
|
|
64
|
-
const executable = await findChrome(options.env ?? process.env);
|
|
65
|
-
if (executable === undefined) throw new CdpBrowserMissingError({ tried: CHROME_CANDIDATES });
|
|
66
|
-
return openLaunched(executable, timeoutMs);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** `undefined` when this machine has no browser. Every other failure still throws. */
|
|
70
|
-
export async function openE2eBrowserIfAvailable(
|
|
71
|
-
options: OpenE2eBrowserOptions = {},
|
|
72
|
-
): Promise<E2eBrowser | undefined> {
|
|
73
|
-
const timeoutMs = budget(options);
|
|
74
|
-
const executable = await findChrome(options.env ?? process.env);
|
|
75
|
-
if (executable === undefined) return undefined;
|
|
76
|
-
return openLaunched(executable, timeoutMs);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* The half both doors share. Each step undoes the ones before it on the way out: a Chrome that
|
|
81
|
-
* launched and then refused the CDP handshake would otherwise be left running, holding its profile
|
|
82
|
-
* directory, for the rest of the test process — one leaked browser per failing suite.
|
|
83
|
-
*/
|
|
84
|
-
async function openLaunched(executable: string, timeoutMs: number): Promise<E2eBrowser> {
|
|
85
|
-
const launched = await launchChrome({ executable, timeoutMs });
|
|
86
|
-
const { connection } = launched;
|
|
87
|
-
try {
|
|
88
|
-
const session = await cdpE2eSession({ connection, loadTimeoutMs: timeoutMs });
|
|
89
|
-
return compose(launched, session, await session.newTab());
|
|
90
|
-
} catch (error) {
|
|
91
|
-
launched.close();
|
|
92
|
-
throw error;
|
|
93
|
-
}
|
|
94
|
-
}
|