@ultimat3/cli 6.0.0 → 8.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 +65 -5
- package/README.md +8 -3
- package/package.json +25 -24
- package/src/affected.ts +320 -0
- package/src/app-boundaries.ts +55 -5
- package/src/bin.ts +6 -3
- package/src/browser-launcher.ts +109 -0
- package/src/ci-log.ts +0 -0
- package/src/ci-runs.ts +179 -0
- package/src/cmd-affected.ts +109 -0
- package/src/cmd-build.ts +29 -3
- package/src/cmd-ci.ts +273 -0
- package/src/cmd-db-backfill.ts +240 -0
- package/src/cmd-db-branch.ts +3 -2
- package/src/cmd-db.ts +35 -156
- package/src/cmd-deploy.ts +37 -3
- package/src/cmd-dev.ts +7 -1
- package/src/cmd-errors.ts +2 -3
- package/src/cmd-fix.ts +3 -3
- package/src/cmd-i18n.ts +67 -5
- package/src/cmd-jobs.ts +27 -4
- package/src/cmd-mcp.ts +18 -9
- package/src/cmd-new.ts +91 -4
- package/src/cmd-policy.ts +3 -2
- package/src/cmd-pr.ts +359 -0
- package/src/cmd-registries.ts +3 -2
- package/src/cmd-shot.ts +382 -0
- package/src/cmd-tasks.ts +9 -4
- package/src/cmd-test.ts +96 -7
- package/src/cmd-verify.ts +47 -6
- package/src/dev-cache.ts +1 -1
- package/src/dev-lock.ts +124 -12
- package/src/dev-queue.ts +12 -7
- package/src/dev-replicator.ts +3 -7
- package/src/dev-roles-fixture.ts +1 -1
- package/src/dev-roles.ts +40 -8
- package/src/dev-runtime.ts +96 -4
- package/src/dev-sync.ts +9 -4
- package/src/dispatch.ts +35 -5
- package/src/drift.ts +52 -7
- package/src/error-codes.ts +21 -0
- package/src/framework-scope.ts +57 -5
- package/src/generate-kinds.ts +19 -1
- package/src/gh-target.ts +118 -0
- package/src/gh.ts +204 -0
- package/src/i18n-registration.ts +67 -4
- package/src/index.ts +38 -1
- package/src/island-bundle.ts +62 -3
- package/src/island-solid-production.ts +129 -0
- package/src/island-styles.ts +41 -0
- package/src/jobs-report.ts +10 -13
- package/src/mcp-errors.ts +12 -0
- package/src/messages.ts +76 -0
- package/src/output.ts +22 -2
- package/src/parse.ts +81 -37
- package/src/pr-threads.ts +291 -0
- package/src/prerender.ts +52 -10
- package/src/realtime-browser-probe-fixture.ts +9 -0
- package/src/registry.ts +8 -0
- package/src/runtime-overrides.ts +11 -3
- package/src/shot-settle.ts +57 -0
- package/src/shot-verdict.ts +360 -0
- package/src/static-report.ts +219 -0
- package/src/sync-authenticator.ts +86 -14
- package/src/templates/guard-bare-error.ts +122 -0
- package/src/templates/guard-raw-colour.ts +138 -0
- package/src/templates/guard-untranslated-string.ts +138 -0
- package/src/templates/guard-unzoned-date.ts +142 -0
- package/src/templates/index.ts +4 -0
- package/src/templates/island-fixture.ts +76 -0
- package/src/templates/island.ts +130 -18
- package/src/templates/resource-form-island.ts +279 -0
- package/src/templates/resource.ts +20 -41
- package/src/templates/route.ts +15 -2
- package/src/templates/scaffold-app.ts +13 -78
- package/src/templates/scaffold-container.ts +30 -4
- package/src/templates/scaffold-db-package.ts +46 -7
- package/src/templates/scaffold-docs.ts +24 -13
- package/src/templates/scaffold-entries.ts +131 -0
- package/src/templates/scaffold-guards.ts +26 -0
- package/src/templates/scaffold-mcp-package.ts +35 -2
- package/src/templates/scaffold-package-shape.ts +7 -2
- package/src/templates/scaffold-repo.ts +37 -6
- package/src/test-select.ts +4 -3
- package/src/test-shards.ts +19 -3
- package/src/verify-checks.ts +11 -1
- package/src/verify-run.ts +25 -3
- package/src/verify-step.ts +11 -2
- package/src/verify-tests.ts +11 -3
- package/src/workspace-graph.ts +241 -0
- package/src/write-line.ts +23 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// The generated app's `packages/db`: the entity re-export list the
|
|
2
|
-
// the deterministic seed. No business logic — that is the package's own stated boundary, and it
|
|
3
|
-
// why `example` reaches only the two files describing the slice's table.
|
|
1
|
+
// The generated app's `packages/db`: the entity re-export list the app's own modules import from
|
|
2
|
+
// and the deterministic seed. No business logic — that is the package's own stated boundary, and it
|
|
3
|
+
// is why `example` reaches only the two files describing the slice's table.
|
|
4
4
|
//
|
|
5
5
|
// No migration. `x db gen` is the ONE writer of `packages/db/migrations`, and a scaffold that hand-
|
|
6
6
|
// wrote `0000_initial.sql` was a second one: it declared a `posts` table the generator had never
|
|
@@ -13,6 +13,37 @@ import { packageShapeFiles, workspacePackageJson } from './scaffold-package-shap
|
|
|
13
13
|
|
|
14
14
|
const DESCRIPTION = 'Entity re-exports and SQL migrations, no business logic';
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* The example slice's entity lives in `apps/web/app/post/`, so `src/schema.ts` re-exports it from
|
|
18
|
+
* there — an edge this manifest has to declare or it exists only inside the root tsconfig's
|
|
19
|
+
* `paths`, where `bun --filter` and every change-detection tool are blind to it
|
|
20
|
+
* (`X_WORKSPACE_DEP_UNDECLARED`). Written here rather than through `workspacePackageJson` for the
|
|
21
|
+
* reason `scaffold-i18n.ts` states: that helper is the dependency-free shape.
|
|
22
|
+
*
|
|
23
|
+
* Under `--no-example` there is no entity and no import, so there is no dependency either: a pin
|
|
24
|
+
* for an edge the package does not have is the same lie in the other direction.
|
|
25
|
+
*/
|
|
26
|
+
const dbPackage = (app: NameSet, example: boolean): string =>
|
|
27
|
+
example
|
|
28
|
+
? `{
|
|
29
|
+
"name": "@${app.kebab}/db",
|
|
30
|
+
"version": "0.0.0",
|
|
31
|
+
"private": true,
|
|
32
|
+
"type": "module",
|
|
33
|
+
"description": "${DESCRIPTION}",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": "./src/index.ts"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"typecheck": "tsc --noEmit -p ../../tsconfig.json"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@${app.kebab}/web": "0.0.0"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
`
|
|
45
|
+
: workspacePackageJson(app, 'db', DESCRIPTION);
|
|
46
|
+
|
|
16
47
|
const dbIndex =
|
|
17
48
|
(): string => `// Schema and migrations only — no business logic lives in this package. The client itself is
|
|
18
49
|
// @ultimat3/db's: one connection pool, sized by ROLE, shared by every package in the app.
|
|
@@ -25,8 +56,16 @@ export * as schema from './schema';
|
|
|
25
56
|
// never written, so each one ships its empty counterpart instead of a reference to a file that is
|
|
26
57
|
// not there — `export { post } from …` alone made `x new --no-example` an app that cannot compile.
|
|
27
58
|
|
|
28
|
-
|
|
29
|
-
//
|
|
59
|
+
// Not "what the migration generator reads" — that claim shipped into every generated app and was
|
|
60
|
+
// false. `x db gen` and `x verify`'s `drift` step both project the ENTITY REGISTRY that `loadApp`
|
|
61
|
+
// fills (`describeEntities()`, `packages/cli/src/app-entities.ts`), so an entity declared anywhere
|
|
62
|
+
// `loadApp` reaches is already in the migration whether or not this file names it. Re-exporting an
|
|
63
|
+
// entity here does exactly one thing, and it is worth doing: it gives `seed.ts` and every other
|
|
64
|
+
// consumer ONE import to reach the app's tables through.
|
|
65
|
+
const SCHEMA_HEADER = `// Every entity the app declares, re-exported here so the seed and anything else that needs a table
|
|
66
|
+
// reach them through one import. It is not what the migration generator reads: \`x db gen\` and the
|
|
67
|
+
// \`drift\` step project the entity registry, so an entity is in the migration because it was
|
|
68
|
+
// declared, never because it was listed here.`;
|
|
30
69
|
|
|
31
70
|
const dbSchema = (app: NameSet, example: boolean): string =>
|
|
32
71
|
example
|
|
@@ -34,7 +73,7 @@ const dbSchema = (app: NameSet, example: boolean): string =>
|
|
|
34
73
|
export { post } from '@${app.kebab}/web/app/post/entity';
|
|
35
74
|
`
|
|
36
75
|
: `${SCHEMA_HEADER}
|
|
37
|
-
// \`x g entity <name>\` writes the entity; add its export here
|
|
76
|
+
// \`x g entity <name>\` writes the entity; add its export here to reach it through \`@${app.kebab}/db\`.
|
|
38
77
|
export {};
|
|
39
78
|
`;
|
|
40
79
|
|
|
@@ -101,7 +140,7 @@ export const ${app.camel}Seed = defineSeed('${app.kebab}', async () => {
|
|
|
101
140
|
|
|
102
141
|
/** Every file the `packages/db` workspace ships, in the order `x new` writes them. */
|
|
103
142
|
export const dbPackageFiles = (app: NameSet, example: boolean): readonly GeneratedFile[] => [
|
|
104
|
-
{ path: 'packages/db/package.json', contents:
|
|
143
|
+
{ path: 'packages/db/package.json', contents: dbPackage(app, example) },
|
|
105
144
|
...packageShapeFiles(app, 'db', DESCRIPTION),
|
|
106
145
|
{ path: 'packages/db/src/index.ts', contents: dbIndex() },
|
|
107
146
|
{ path: 'packages/db/src/schema.ts', contents: dbSchema(app, example) },
|
|
@@ -13,19 +13,30 @@ const agents = (app: NameSet): string => `# AGENTS.md
|
|
|
13
13
|
Human-authored, short, stable. Facts live in \`x.manifest.json\`; this file holds only what an
|
|
14
14
|
agent cannot infer from the code.
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
|
20
|
-
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
28
|
-
|
|
16
|
+
Every row below names what refuses it. A rule with nothing in the last column is a rule that does
|
|
17
|
+
not exist — five of these had an empty column and were each measured green on \`x verify\`.
|
|
18
|
+
|
|
19
|
+
| Rule | Detail | Refused by |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| One gate | \`x verify\` — green means shippable. Never merge red. | the gate itself |
|
|
22
|
+
| One way | generators, not hand-rolled files: \`x g resource\`, \`x g action\`, \`x g route\` | review |
|
|
23
|
+
| Surfaces | \`site/\` is 0kb JS and may not import \`app/\`; \`shared/\` is a leaf | \`X_BOUNDARY_SITE_TO_APP\` |
|
|
24
|
+
| Data | routes call actions and queries; only \`repo.ts\` touches the database | \`X_BOUNDARY_ROUTE_TO_DB\` |
|
|
25
|
+
| Errors | never \`throw new Error\` — subclass \`UltimateError\` with a code, a cause and a fix | \`guards/bare-error.ts\` |
|
|
26
|
+
| Money | \`{ minor, currency }\`, never a float — \`money()\` on the column | the type: \`price: 19.99\` is TS2322, \`number\` is not \`MoneyInput\` |
|
|
27
|
+
| Time | store UTC, format with an explicit IANA time zone | \`guards/unzoned-date.ts\` |
|
|
28
|
+
| Strings | every user-facing string goes through \`t()\` | \`guards/untranslated-string.ts\` |
|
|
29
|
+
| Colour | semantic tokens only, never a raw hex | \`guards/raw-colour.ts\` |
|
|
30
|
+
|
|
31
|
+
\`guards/\` is yours: each file is one rule, discovered by \`x verify\` and run inside its
|
|
32
|
+
\`boundaries\` step. Delete one to drop the rule, and \`x g guard <name>\` writes the next.
|
|
33
|
+
|
|
34
|
+
Money is the one row with no guard, deliberately: a float has no static signature a text rule can
|
|
35
|
+
see, and the type already fires — measured, \`price: 19.99\` in a seed is
|
|
36
|
+
\`TS2322: Type 'number' is not assignable to type 'MoneyInput'\`. A guard that pretended to check
|
|
37
|
+
it would be worse than the type that really does.
|
|
38
|
+
|
|
39
|
+
Commands: \`x dev\`, \`x verify\`, \`x g <primitive>\`, \`x g guard <name>\`, \`x db branch create <name>\`, \`x doctor\`.
|
|
29
40
|
|
|
30
41
|
Project notes for ${app.kebab}: replace this line with the conventions a newcomer could not guess.
|
|
31
42
|
`;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// The two entry files a deploy needs — `apps/web/server.ts` and `apps/web/prerender.ts`. Split out
|
|
2
|
+
// of `scaffold-app.ts` because that file is the three SURFACES and these are neither: they are what
|
|
3
|
+
// a container starts and what a CDN is handed. Both are deliberately thin — which role a container
|
|
4
|
+
// is, which port it binds, how it drains and what a static build enumerates are the framework's
|
|
5
|
+
// answers, so an upgrade moves them without a codemod in every app that ever shipped.
|
|
6
|
+
|
|
7
|
+
import type { GeneratedFile } from './naming';
|
|
8
|
+
|
|
9
|
+
const server =
|
|
10
|
+
(): string => `// The production entry. \`docker/Dockerfile\` starts this, and \`x build --target binary\` compiles it.
|
|
11
|
+
// ROLE selects what this process is — web, sync, worker, scheduler, replicator, or migrate, which
|
|
12
|
+
// applies the migrations and exits. PORT is bound on every interface, because a container bound to
|
|
13
|
+
// localhost is unreachable through its own port mapping.
|
|
14
|
+
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { runRole } from '@ultimat3/cli';
|
|
17
|
+
|
|
18
|
+
// MORE THAN ONE REPLICA? Add these two lines, above \`runRole\`:
|
|
19
|
+
//
|
|
20
|
+
// import { configureIdempotency } from '@ultimat3/action';
|
|
21
|
+
// configureIdempotency({ scope: 'shared' });
|
|
22
|
+
//
|
|
23
|
+
// \`idempotent: true\` on an action promises that a retry does not repeat the work. Under the
|
|
24
|
+
// process-scoped default that promise holds inside ONE process — a client retrying
|
|
25
|
+
// \`POST /api/payments/charge\` after a timeout lands on another replica, which has never seen the
|
|
26
|
+
// key, and charges the card twice, silently, with \`x verify\` green. Declaring \`'shared'\` is what
|
|
27
|
+
// makes that a boot error (\`X_IDEMPOTENCY_NOT_SHARED\`) unless a shared store is installed.
|
|
28
|
+
// \`runRole\` installs the Postgres one for you, on the connection it resolved from \`DATABASE_URL\`,
|
|
29
|
+
// so the declaration is all this app owes. It must run before \`runRole\` imports the actions.
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Where the app is. From this file normally — the image's WORKDIR is not the app root's business.
|
|
33
|
+
* A \`--compile\` binary is the exception: its \`import.meta.dir\` is Bun's virtual filesystem, which
|
|
34
|
+
* holds this module's bundled imports and none of the app's source, and the framework's registries
|
|
35
|
+
* are filled by scanning that source at boot. So a binary reads its root from the directory it is
|
|
36
|
+
* started in — it is a launcher for an app tree, not a self-contained copy of one.
|
|
37
|
+
*/
|
|
38
|
+
const root = import.meta.dir.startsWith('/$bunfs')
|
|
39
|
+
? process.cwd()
|
|
40
|
+
: join(import.meta.dir, '..', '..');
|
|
41
|
+
|
|
42
|
+
// Guarded, because the framework's module scan imports every file under apps/*/ to fill its
|
|
43
|
+
// registries — an unguarded boot would start a server inside \`x verify\`.
|
|
44
|
+
if (import.meta.main) {
|
|
45
|
+
await runRole({ root, env: Bun.env });
|
|
46
|
+
}
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
const prerender =
|
|
50
|
+
(): string => `// The static entry. \`x build --target static\` runs this with \`--out <dir>\` and it writes one HTML
|
|
51
|
+
// file per \`render: 'static'\` route — a CDN or an object store then serves site/ with no process
|
|
52
|
+
// behind it. Every other render mode needs a running app and is reported as skipped, never emitted.
|
|
53
|
+
//
|
|
54
|
+
// Skipped is not unweighed: a route that declares a \`budget:\` is rendered in memory and measured
|
|
55
|
+
// whatever its mode, so \`x verify\`'s \`budgets\` step has a number for it. \`unmeasured\` is the list
|
|
56
|
+
// this build could not render — each one is an X_BUDGET_UNMEASURED at the gate, and this is where
|
|
57
|
+
// the reason is.
|
|
58
|
+
//
|
|
59
|
+
// It writes the whole of \`pages\` and \`skipped\`, never a COUNT of either. A count is what let a
|
|
60
|
+
// partial artifact read as a complete one: someone pointed a screenshot tool at \`.x/static\` and
|
|
61
|
+
// filed "the island did not mount" against a route that had never been emitted (issue #242). Each
|
|
62
|
+
// skipped route carries its own \`reason\` and \`why\`, and \`report\` is where the same inventory
|
|
63
|
+
// landed on disk — which is what \`x build --target static --json\` reads back.
|
|
64
|
+
|
|
65
|
+
import { join } from 'node:path';
|
|
66
|
+
import { DEFAULT_ORIGIN, type PrerenderReport, prerenderSite } from '@ultimat3/cli';
|
|
67
|
+
import { routeEntries } from '@ultimat3/render';
|
|
68
|
+
import { buildRobots, buildSitemap, type RouteRecord } from '@ultimat3/seo';
|
|
69
|
+
|
|
70
|
+
const root = join(import.meta.dir, '..', '..');
|
|
71
|
+
const flag = Bun.argv.indexOf('--out');
|
|
72
|
+
const out = (flag === -1 ? undefined : Bun.argv[flag + 1]) ?? join(root, '.x', 'static');
|
|
73
|
+
// SITE_ORIGIN is what canonical and og:url are built from; the default is only ever a local build.
|
|
74
|
+
// Property access, not \`Bun.env['SITE_ORIGIN']\`: the scaffolded tsconfig does not set
|
|
75
|
+
// \`noPropertyAccessFromIndexSignature\`, so the bracket form is the one biome's useLiteralKeys
|
|
76
|
+
// reports — a diagnostic in an app's first lint run over a file the app never wrote.
|
|
77
|
+
const origin = Bun.env.SITE_ORIGIN;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The route table as \`@ultimat3/seo\` reads it. \`RouteRecord\` is a static row and \`defineRoute\`
|
|
81
|
+
* is a live declaration, so one has to be projected onto the other — and \`prerenderSite\` has
|
|
82
|
+
* already loaded the app by the time this runs, which is what fills \`routeEntries()\`.
|
|
83
|
+
*
|
|
84
|
+
* A DYNAMIC route contributes exactly the URLs this build enumerated for it, read back off the
|
|
85
|
+
* report rather than by calling \`prerender()\` a second time: the sitemap then cannot name a page
|
|
86
|
+
* the artifact does not contain, which is the only failure mode a sitemap really has.
|
|
87
|
+
*/
|
|
88
|
+
const siteRoutes = (report: PrerenderReport): readonly RouteRecord[] =>
|
|
89
|
+
routeEntries()
|
|
90
|
+
.filter((entry) => entry.surface === 'site')
|
|
91
|
+
.map((entry) => ({
|
|
92
|
+
path: entry.path,
|
|
93
|
+
file: entry.file,
|
|
94
|
+
surface: 'site' as const,
|
|
95
|
+
render: entry.config.render,
|
|
96
|
+
prerender: () =>
|
|
97
|
+
report.pages.filter((page) => page.route === entry.path).map((page) => page.path),
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* \`sitemap.xml\` and \`robots.txt\`, into the same directory the HTML went. Both belong to the
|
|
102
|
+
* ARTIFACT rather than to a request: a static export is served with no process behind it, so a
|
|
103
|
+
* route that answered them at run time would be a file the CDN never has.
|
|
104
|
+
*
|
|
105
|
+
* \`buildRobots\` fails closed — anything that is not \`ULTIMATE_ENV=production\` emits
|
|
106
|
+
* \`Disallow: /\` and advertises no sitemap — so a preview build cannot outrank the real site.
|
|
107
|
+
*/
|
|
108
|
+
async function writeSeoFiles(report: PrerenderReport, baseUrl: string): Promise<readonly string[]> {
|
|
109
|
+
const sitemap = await buildSitemap(siteRoutes(report), { baseUrl });
|
|
110
|
+
// Past 50,000 URLs \`files\` are \`/sitemap-N.xml\` and the index is \`/sitemap.xml\`; below it,
|
|
111
|
+
// \`files\` is that one file and there is no index. Writing both lists covers each case once.
|
|
112
|
+
const written = sitemap.index === undefined ? sitemap.files : [sitemap.index, ...sitemap.files];
|
|
113
|
+
for (const file of written) await Bun.write(join(out, file.path), file.xml);
|
|
114
|
+
await Bun.write(join(out, 'robots.txt'), buildRobots({ baseUrl, sitemaps: ['/sitemap.xml'] }));
|
|
115
|
+
return [...written.map((file) => file.path), '/robots.txt'];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (import.meta.main) {
|
|
119
|
+
const report = await prerenderSite({ root, out, ...(origin === undefined ? {} : { origin }) });
|
|
120
|
+
const seo = await writeSeoFiles(report, origin ?? DEFAULT_ORIGIN);
|
|
121
|
+
await Bun.stdout.write(
|
|
122
|
+
\`\${JSON.stringify({ ok: true, out: report.out, emitted: report.pages, skipped: report.skipped, unmeasured: report.unmeasured, report: report.report, seo })}\\n\`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
`;
|
|
126
|
+
|
|
127
|
+
/** The two files, in the order a deploy meets them: the process, then the artifact. */
|
|
128
|
+
export const entryFiles = (): readonly GeneratedFile[] => [
|
|
129
|
+
{ path: 'apps/web/server.ts', contents: server() },
|
|
130
|
+
{ path: 'apps/web/prerender.ts', contents: prerender() },
|
|
131
|
+
];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// The `guards/` directory `x new` writes, and the one list that names every guard in it.
|
|
2
|
+
//
|
|
3
|
+
// `x new` shipped ZERO guards while the `AGENTS.md` it writes stated nine non-negotiables — five of
|
|
4
|
+
// which nothing enforced, each measured green on `x verify` in a scaffolded app. The mechanism was
|
|
5
|
+
// never missing: `guards/` is discovered, not registered, and runs inside the `boundaries` step
|
|
6
|
+
// (`packages/cli/src/guards.ts`). What was missing is any guard to discover. Four of the five are
|
|
7
|
+
// here; the fifth, money-as-float, has no static signature and is answered by the `Money` type
|
|
8
|
+
// instead — `scaffold-docs.ts` says so where an author reads it.
|
|
9
|
+
|
|
10
|
+
import { bareErrorGuardFiles } from './guard-bare-error';
|
|
11
|
+
import { rawColourGuardFiles } from './guard-raw-colour';
|
|
12
|
+
import { untranslatedStringGuardFiles } from './guard-untranslated-string';
|
|
13
|
+
import { unzonedDateGuardFiles } from './guard-unzoned-date';
|
|
14
|
+
import type { GeneratedFile } from './naming';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Every guard `x new` ships, each with its test. One module per guard, because an app deletes a
|
|
18
|
+
* rule it does not want by deleting one file — and a guard the app then writes for itself is
|
|
19
|
+
* `x g guard <name>`, the same shape.
|
|
20
|
+
*/
|
|
21
|
+
export const scaffoldGuardFiles = (): readonly GeneratedFile[] => [
|
|
22
|
+
...bareErrorGuardFiles(),
|
|
23
|
+
...rawColourGuardFiles(),
|
|
24
|
+
...untranslatedStringGuardFiles(),
|
|
25
|
+
...unzonedDateGuardFiles(),
|
|
26
|
+
];
|
|
@@ -3,10 +3,43 @@
|
|
|
3
3
|
// itself.
|
|
4
4
|
|
|
5
5
|
import type { GeneratedFile, NameSet } from './naming';
|
|
6
|
-
import { packageShapeFiles
|
|
6
|
+
import { packageShapeFiles } from './scaffold-package-shape';
|
|
7
7
|
|
|
8
8
|
const DESCRIPTION = "The app's own MCP tools";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Writes its own manifest, for the reason `scaffold-i18n.ts` does: `workspacePackageJson` is the
|
|
12
|
+
* dependency-free shape, and this package has a real dependency — `src/index.ts` below imports the
|
|
13
|
+
* app's actions out of `apps/web/api`, because `registerActions` has to see them.
|
|
14
|
+
*
|
|
15
|
+
* The edge points AT the app, which is unusual and correct. `apps/web` is a workspace like any
|
|
16
|
+
* other; reversing it would put the tool catalog upstream of the actions it projects. Declared
|
|
17
|
+
* rather than left to the root tsconfig's `paths`: an undeclared edge resolves for `tsc` and for
|
|
18
|
+
* nothing else — not for `bun --filter` ordering, not for any tool asking what a change affects
|
|
19
|
+
* (`X_WORKSPACE_DEP_UNDECLARED`).
|
|
20
|
+
*
|
|
21
|
+
* `"0.0.0"` and not `workspace:*`: it is the version `apps/web` really carries, which is what
|
|
22
|
+
* `checkLockstep` compares a sibling pin against, and it is the one spelling every other manifest
|
|
23
|
+
* `x new` writes already uses.
|
|
24
|
+
*/
|
|
25
|
+
const mcpPackage = (app: NameSet): string => `{
|
|
26
|
+
"name": "@${app.kebab}/mcp",
|
|
27
|
+
"version": "0.0.0",
|
|
28
|
+
"private": true,
|
|
29
|
+
"type": "module",
|
|
30
|
+
"description": "${DESCRIPTION}",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": "./src/index.ts"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"typecheck": "tsc --noEmit -p ../../tsconfig.json"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@${app.kebab}/web": "0.0.0"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
`;
|
|
42
|
+
|
|
10
43
|
const mcpIndex = (
|
|
11
44
|
app: NameSet,
|
|
12
45
|
): string => `// The app's own MCP tools. Every action with mcp.expose is already a tool; add app-specific
|
|
@@ -42,7 +75,7 @@ unitTest('the app exposes its actions as MCP tools', () => {
|
|
|
42
75
|
|
|
43
76
|
/** Every file the `packages/mcp` workspace ships, in the order `x new` writes them. */
|
|
44
77
|
export const mcpPackageFiles = (app: NameSet): readonly GeneratedFile[] => [
|
|
45
|
-
{ path: 'packages/mcp/package.json', contents:
|
|
78
|
+
{ path: 'packages/mcp/package.json', contents: mcpPackage(app) },
|
|
46
79
|
...packageShapeFiles(app, 'mcp', DESCRIPTION),
|
|
47
80
|
{ path: 'packages/mcp/src/index.ts', contents: mcpIndex(app) },
|
|
48
81
|
{ path: 'packages/mcp/src/index.test.ts', contents: mcpTest() },
|
|
@@ -50,8 +50,13 @@ export const packageShapeFiles = (
|
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
52
|
* The manifest every scaffolded `packages/*` carries. Private, versionless-by-convention and
|
|
53
|
-
* dependency-free: these packages only re-export a framework package's types
|
|
54
|
-
*
|
|
53
|
+
* dependency-free: these packages only re-export a framework package's types. Three now DO name a
|
|
54
|
+
* dependency and each writes its own manifest — `scaffold-i18n.ts`, `scaffold-mcp-package.ts` and
|
|
55
|
+
* `scaffold-db-package.ts` (the last only under `--example`, which is the branch that emits the
|
|
56
|
+
* import). Each says why at its own site. They write their own rather than taking a `dependencies`
|
|
57
|
+
* parameter here because the edge is a fact about the SOURCE that template emits, and a parameter
|
|
58
|
+
* would let a caller declare an edge its generated code does not have — which is the drift
|
|
59
|
+
* `X_WORKSPACE_DEP_UNDECLARED` exists to catch, pointed the other way. Lives beside
|
|
55
60
|
* `packageShapeFiles` because every caller of one calls the other.
|
|
56
61
|
*/
|
|
57
62
|
export const workspacePackageJson = (app: NameSet, name: string, description: string): string => `{
|
|
@@ -12,6 +12,7 @@ import { dbPackageFiles } from './scaffold-db-package';
|
|
|
12
12
|
import { docsFiles } from './scaffold-docs';
|
|
13
13
|
import { domainPackageFiles } from './scaffold-domain-package';
|
|
14
14
|
import { envExampleSource, envSchemaSource } from './scaffold-env';
|
|
15
|
+
import { scaffoldGuardFiles } from './scaffold-guards';
|
|
15
16
|
import { i18nFiles } from './scaffold-i18n';
|
|
16
17
|
import { mcpPackageFiles } from './scaffold-mcp-package';
|
|
17
18
|
import { uiPackageFiles } from './scaffold-ui-package';
|
|
@@ -70,6 +71,7 @@ const rootPackage = (app: NameSet, version: string): string => `{
|
|
|
70
71
|
"@ultimat3/query": "^${version}",
|
|
71
72
|
"@ultimat3/render": "^${version}",
|
|
72
73
|
"@ultimat3/schema": "^${version}",
|
|
74
|
+
"@ultimat3/seo": "^${version}",
|
|
73
75
|
"@ultimat3/ui": "^${version}",
|
|
74
76
|
"solid-js": "1.9.14"
|
|
75
77
|
},
|
|
@@ -121,11 +123,13 @@ const envDeclaration = (): string => `${envSchemaSource()}
|
|
|
121
123
|
export const env = defineEnv(envSchema);`;
|
|
122
124
|
|
|
123
125
|
/**
|
|
124
|
-
* No `installPrompt
|
|
125
|
-
* `
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
126
|
+
* No `installPrompt`, no `afterSignInPath`, no `modelEnv`. All three were declared by
|
|
127
|
+
* `defineConfig`, defaulted by it, and read by NO file; all three are DELETED from
|
|
128
|
+
* `packages/core/src/config.ts` as of 2026-08-22, whose header now records the removal rather than
|
|
129
|
+
* the marker this comment used to cite. So scaffolding one is no longer a switch with no wire — it
|
|
130
|
+
* is `TS2353` in the generated app's first `x verify`. The note belongs HERE rather than in the
|
|
131
|
+
* emitted file: an app author has no use for a comment about keys their config does not name.
|
|
132
|
+
* `scaffold-config.test.ts` is what keeps them from growing back.
|
|
129
133
|
*/
|
|
130
134
|
const appConfig = (
|
|
131
135
|
app: NameSet,
|
|
@@ -164,10 +168,34 @@ export const config = defineConfig({
|
|
|
164
168
|
// otherwise fail `lint` on a file no author typed and `x db gen` would rewrite anyway.
|
|
165
169
|
// `preset`, not `recommended`: the older key is deprecated from 2.5 on and every `bun run lint`
|
|
166
170
|
// in the scaffolded app printed the migration notice for a config the app never wrote by hand.
|
|
171
|
+
//
|
|
172
|
+
// `!**/.x` is the one that makes the app's fix chain terminate. `x build` writes MINIFIED island
|
|
173
|
+
// bundles to `.x/static/islands/<name>-<contenthash>.js`; without the exclusion `lint` reported
|
|
174
|
+
// ~175 `noCommaOperator`/`noAssignInExpressions` errors in Bun's own output, and the `fix:` for
|
|
175
|
+
// that step — `biome check --write .` — still exits 1, so `x verify` was red forever on a pristine
|
|
176
|
+
// scaffold the moment `x build` ran (which the `budgets` step's own `fix:` tells the author to do).
|
|
177
|
+
// `--unsafe` was worse: it REWROTE a content-hashed chunk in place, 55,499 → 83,605 bytes, so the
|
|
178
|
+
// name no longer matched the bytes and `.x/build-stats.json` no longer matched the artifact.
|
|
179
|
+
// `--no-example` hid it, because an app with no island has nothing under `.x/static/islands`.
|
|
180
|
+
//
|
|
181
|
+
// `vcs.useIgnoreFile` is the second half and not a duplicate of the first: it makes every future
|
|
182
|
+
// generated directory the app adds to `.gitignore` excluded by the act of ignoring it, rather than
|
|
183
|
+
// by an edit to this file nobody will remember to make. It needs `.gitignore` to EXIST — Biome
|
|
184
|
+
// exits 1 with `couldn't find an ignore file` when it does not — which `x new` writes, and which
|
|
185
|
+
// is why the two are declared together rather than one of them alone.
|
|
167
186
|
const biome = (): string => `{
|
|
168
187
|
"$schema": "https://biomejs.dev/schemas/${BIOME_VERSION}/schema.json",
|
|
188
|
+
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
|
|
169
189
|
"files": {
|
|
170
|
-
"includes": [
|
|
190
|
+
"includes": [
|
|
191
|
+
"**",
|
|
192
|
+
"!**/.x",
|
|
193
|
+
"!**/dist",
|
|
194
|
+
"!**/.output",
|
|
195
|
+
"!**/migrations",
|
|
196
|
+
"!x.manifest.json",
|
|
197
|
+
"!openapi.json"
|
|
198
|
+
]
|
|
171
199
|
},
|
|
172
200
|
"formatter": { "indentStyle": "space", "indentWidth": 2, "lineWidth": 100 },
|
|
173
201
|
"linter": {
|
|
@@ -304,6 +332,9 @@ export function repoFiles(
|
|
|
304
332
|
// One call per workspace package, in write order. Each owns its own files (`scaffold-i18n.ts`
|
|
305
333
|
// already did), so this list stays a table of contents rather than a second copy of every
|
|
306
334
|
// package's contents.
|
|
335
|
+
// The app's own conventions, as build errors. Not a package — `guards/` sits at the repo root
|
|
336
|
+
// because the gate discovers it there, and every rule it holds is about the whole app.
|
|
337
|
+
...scaffoldGuardFiles(),
|
|
307
338
|
...domainPackageFiles(app),
|
|
308
339
|
...dbPackageFiles(app, example),
|
|
309
340
|
...i18nFiles(app, version),
|
package/src/test-select.ts
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
// validation behind the type positional and `--sample`. Selection only — nothing here splits a
|
|
3
3
|
// shard or spawns a process, so a wrong file list is always this file's bug and never a race.
|
|
4
4
|
|
|
5
|
+
import { join } from 'node:path';
|
|
5
6
|
// Bun ships no equivalent: `join` builds the host-separator path from the scan root to a hit.
|
|
6
7
|
// Sizing is Bun's own (`Bun.file().size`), so nothing here reaches for `node:fs`.
|
|
7
|
-
import {
|
|
8
|
+
import { nearestName } from '@ultimat3/core';
|
|
8
9
|
import { BadFlagError } from './errors';
|
|
9
10
|
import type { ParsedArgs } from './parse';
|
|
10
|
-
import { flagString
|
|
11
|
+
import { flagString } from './parse';
|
|
11
12
|
import type { TestType } from './verify-tests';
|
|
12
13
|
import { ownerOf, TEST_TYPES } from './verify-tests';
|
|
13
14
|
|
|
@@ -103,7 +104,7 @@ export function readType(raw: string | undefined): TestType | undefined {
|
|
|
103
104
|
if (raw === undefined) return undefined;
|
|
104
105
|
const known: readonly string[] = TEST_TYPES;
|
|
105
106
|
if (known.includes(raw)) return raw as TestType;
|
|
106
|
-
const suggestion =
|
|
107
|
+
const suggestion = nearestName(raw, known);
|
|
107
108
|
throw new BadFlagError({
|
|
108
109
|
flag: 'type',
|
|
109
110
|
command: 'test',
|
package/src/test-shards.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// cmd-test.ts because a printed reproduction is only true if it carries every input to the split —
|
|
4
4
|
// that rule is this file's, and argv parsing is that one's.
|
|
5
5
|
|
|
6
|
+
import type { AffectedSelection } from './affected';
|
|
6
7
|
import { docsFor } from './error-codes';
|
|
7
8
|
import type { Runner } from './exec';
|
|
8
9
|
import { execOutput } from './exec';
|
|
@@ -72,12 +73,18 @@ export interface ReproduceOptions {
|
|
|
72
73
|
readonly type?: TestType;
|
|
73
74
|
/** Files `--sample` kept, so the rerun samples the same corpus instead of the whole type. */
|
|
74
75
|
readonly sample?: number;
|
|
76
|
+
/**
|
|
77
|
+
* The `--affected` narrowing, when there was one. The fourth input to the split and the one most
|
|
78
|
+
* easily forgotten: `--affected` decides which files exist to shard at all, so a rerun without it
|
|
79
|
+
* re-splits the WHOLE corpus and its shard 2 is a different shard 2.
|
|
80
|
+
*/
|
|
81
|
+
readonly affected?: AffectedSelection;
|
|
75
82
|
}
|
|
76
83
|
|
|
77
84
|
/**
|
|
78
|
-
* Every input to the split, printed back. The type and `--
|
|
79
|
-
* shard, `--sample` decides how many of them survive, `--workers` decides the bins — drop
|
|
80
|
-
* and the command still runs, over a different file set, which reproduces nothing.
|
|
85
|
+
* Every input to the split, printed back. The type, `--filter` and `--affected` decide which files
|
|
86
|
+
* exist to shard, `--sample` decides how many of them survive, `--workers` decides the bins — drop
|
|
87
|
+
* any one and the command still runs, over a different file set, which reproduces nothing.
|
|
81
88
|
*/
|
|
82
89
|
export function reproduceFor(shard: Shard, options: ReproduceOptions): string {
|
|
83
90
|
return [
|
|
@@ -85,6 +92,12 @@ export function reproduceFor(shard: Shard, options: ReproduceOptions): string {
|
|
|
85
92
|
...(options.type === undefined ? [] : [quoteArg(options.type)]),
|
|
86
93
|
...(options.filter === undefined ? [] : ['--filter', quoteArg(options.filter)]),
|
|
87
94
|
...(options.sample === undefined ? [] : ['--sample', String(options.sample)]),
|
|
95
|
+
// `--base` is emitted always rather than only when non-default: the default is `main`, and a
|
|
96
|
+
// rerun days later against a moved `main` is a different diff wearing the same flag.
|
|
97
|
+
...(options.affected === undefined
|
|
98
|
+
? []
|
|
99
|
+
: ['--affected', '--base', quoteArg(options.affected.base)]),
|
|
100
|
+
...(options.affected?.dirty === true ? ['--dirty'] : []),
|
|
88
101
|
'--workers',
|
|
89
102
|
String(options.workers),
|
|
90
103
|
'--worker',
|
|
@@ -107,6 +120,8 @@ export interface RunShardsOptions {
|
|
|
107
120
|
* shard of the sample and would otherwise report that shard's size as the corpus.
|
|
108
121
|
*/
|
|
109
122
|
readonly sample?: { readonly kept: number; readonly total: number };
|
|
123
|
+
/** Passed straight to `reproduceFor`: see `ReproduceOptions.affected`. */
|
|
124
|
+
readonly affected?: AffectedSelection;
|
|
110
125
|
}
|
|
111
126
|
|
|
112
127
|
/** The reproduction's inputs, resolved once: `workers` is the split's real width, not the ask. */
|
|
@@ -115,6 +130,7 @@ const planOf = (options: RunShardsOptions, workers: number): ReproduceOptions =>
|
|
|
115
130
|
...(options.filter === undefined ? {} : { filter: options.filter }),
|
|
116
131
|
...(options.type === undefined ? {} : { type: options.type }),
|
|
117
132
|
...(options.sample === undefined ? {} : { sample: options.sample.kept }),
|
|
133
|
+
...(options.affected === undefined ? {} : { affected: options.affected }),
|
|
118
134
|
});
|
|
119
135
|
|
|
120
136
|
const failureOf = (shard: Shard, code: number, plan: ReproduceOptions): Finding => ({
|
package/src/verify-checks.ts
CHANGED
|
@@ -35,6 +35,7 @@ import type { VerifyStep } from './verify-step';
|
|
|
35
35
|
import { fromExec, fromFindings, hostFindings } from './verify-step';
|
|
36
36
|
import { TEST_STEPS } from './verify-tests';
|
|
37
37
|
import { checkFileSizes, checkPackageShape, hasWorkspacePackages } from './workspace-checks';
|
|
38
|
+
import { checkWorkspaceDependencies } from './workspace-graph';
|
|
38
39
|
|
|
39
40
|
/** The one file that makes the `roadmap` step answerable, and therefore what `applies` reads. */
|
|
40
41
|
const ROADMAP_FILE = join('docs', 'idea', '14-roadmap.md');
|
|
@@ -100,7 +101,16 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
100
101
|
name: 'package-shape',
|
|
101
102
|
summary: 'every package ships the same contract files',
|
|
102
103
|
applies: (ctx) => hasWorkspacePackages(ctx.root),
|
|
103
|
-
|
|
104
|
+
// The dependency rule rides here rather than becoming a twentieth step because it is this
|
|
105
|
+
// step's own question — what does a workspace owe the repo it lives in? — asked of the
|
|
106
|
+
// manifest's `dependencies` instead of its `files`. It is deliberately NOT inside
|
|
107
|
+
// `checkPackageShape`: `scripts/release.ts --check` calls that one to ask whether the tree is
|
|
108
|
+
// at the version a tag claims, and an undeclared import is not that question.
|
|
109
|
+
run: async (ctx) =>
|
|
110
|
+
fromFindings([
|
|
111
|
+
...(await checkPackageShape(ctx.root)),
|
|
112
|
+
...(await checkWorkspaceDependencies(ctx.root)),
|
|
113
|
+
]),
|
|
104
114
|
},
|
|
105
115
|
{
|
|
106
116
|
name: 'errors',
|
package/src/verify-run.ts
CHANGED
|
@@ -16,6 +16,10 @@ import type { StepOutcome, VerifyContext, VerifyStep } from './verify-step';
|
|
|
16
16
|
/**
|
|
17
17
|
* Run every step in order, never bailing early: an agent fixing three things at once needs all
|
|
18
18
|
* three findings from one run, not one per round-trip.
|
|
19
|
+
*
|
|
20
|
+
* `ctx.only` narrows the list to one step. The narrowing lives HERE rather than in `cmd-verify.ts`
|
|
21
|
+
* so that every caller of the runner — the command, `x build`, the MCP host — gets the banner and
|
|
22
|
+
* the `--json` flag with it, instead of one of them filtering a list quietly.
|
|
19
23
|
*/
|
|
20
24
|
export async function runVerify(
|
|
21
25
|
steps: readonly VerifyStep[],
|
|
@@ -23,7 +27,8 @@ export async function runVerify(
|
|
|
23
27
|
): Promise<CommandResult> {
|
|
24
28
|
const floor = await readVerifyFloor(ctx.root);
|
|
25
29
|
const results: StepResult[] = [];
|
|
26
|
-
|
|
30
|
+
const selected = ctx.only === undefined ? steps : steps.filter((step) => step.name === ctx.only);
|
|
31
|
+
for (const step of selected) {
|
|
27
32
|
const applies = step.applies === undefined ? true : await step.applies(ctx);
|
|
28
33
|
if (!applies) {
|
|
29
34
|
// A skip this repo already ruled out is not a skip. The step ran here before — the floor is
|
|
@@ -67,14 +72,31 @@ export async function runVerify(
|
|
|
67
72
|
const failedSteps = results.filter((step) => !step.ok).map((step) => step.name);
|
|
68
73
|
const skippedSteps = results.filter((step) => step.skipped === true).map((step) => step.name);
|
|
69
74
|
const totalMs = results.reduce((sum, step) => sum + step.durationMs, 0);
|
|
75
|
+
const summary = verifySummary({
|
|
76
|
+
results,
|
|
77
|
+
failed: failedSteps,
|
|
78
|
+
skipped: skippedSteps,
|
|
79
|
+
totalMs,
|
|
80
|
+
});
|
|
70
81
|
return {
|
|
71
82
|
ok: failedSteps.length === 0,
|
|
72
83
|
command: 'verify',
|
|
73
|
-
|
|
84
|
+
// Rendered through the catalog like every other summary this file emits; the machine marker
|
|
85
|
+
// is `data.notAGateRun` below. It was a bare `NOT A GATE RUN` constant, which put one
|
|
86
|
+
// user-facing string outside `messages.ts` for a fact `--json` was already carrying twice.
|
|
87
|
+
summary: ctx.only === undefined ? summary : msg('cli.verify.notAGateRun', { summary }),
|
|
74
88
|
steps: results,
|
|
75
89
|
// `skipped` is a list beside `failed` and not a count, because the two answer the same kind of
|
|
76
90
|
// question — *which* steps, not how many — and a caller ratcheting on coverage needs the names.
|
|
77
|
-
data: {
|
|
91
|
+
data: {
|
|
92
|
+
failed: failedSteps,
|
|
93
|
+
skipped: skippedSteps,
|
|
94
|
+
durationMs: totalMs,
|
|
95
|
+
// A BOOLEAN beside the banner, so a reader of `--json` never has to substring-match a
|
|
96
|
+
// summary line to learn that this run checked one thing.
|
|
97
|
+
...(ctx.only === undefined ? {} : { notAGateRun: true, only: ctx.only }),
|
|
98
|
+
},
|
|
99
|
+
// The step's own status: one step, so `failedSteps` is that step and nothing else.
|
|
78
100
|
exitCode: failedSteps.length === 0 ? 0 : 1,
|
|
79
101
|
};
|
|
80
102
|
}
|
package/src/verify-step.ts
CHANGED
|
@@ -59,10 +59,19 @@ export interface VerifyContext {
|
|
|
59
59
|
readonly hostChecks?: Partial<Record<VerifyStepName, HostCheck>>;
|
|
60
60
|
/**
|
|
61
61
|
* How wide the parallel test steps go. Absent means `defaultWorkers()` — a knob, never a
|
|
62
|
-
* narrowing: no value of it changes which steps run or what "green" means
|
|
63
|
-
* the only flag `x verify` accepts beyond the global ones.
|
|
62
|
+
* narrowing: no value of it changes which steps run or what "green" means.
|
|
64
63
|
*/
|
|
65
64
|
readonly workers?: number;
|
|
65
|
+
/**
|
|
66
|
+
* ONE step, by name — an iteration loop, and the one thing here that IS a narrowing. Every
|
|
67
|
+
* iteration of the whole gate costs ~18s (14s of it `tsc -b`), which is the cost of asking a
|
|
68
|
+
* question about one step. It does not weaken axiom 5, and the two rules that keep it honest are
|
|
69
|
+
* mechanical rather than remembered: a run with this set prints `NOT A GATE RUN` in the summary
|
|
70
|
+
* AND carries `notAGateRun` in `--json` (`verify-run.ts`), so no reader of either can mistake it
|
|
71
|
+
* for the gate; and nothing writes `x.verify.json`, so the suite floor cannot be lowered by a
|
|
72
|
+
* run that never executed the suites. Green still means the no-flag run, unchanged.
|
|
73
|
+
*/
|
|
74
|
+
readonly only?: VerifyStepName;
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
export interface StepOutcome {
|