@ultimat3/cli 19.3.2 → 19.4.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/package.json +29 -29
- package/src/dev-roles.ts +25 -7
- package/src/dev-sync.ts +74 -6
- package/src/verify-checks.ts +9 -2
- package/src/verify-floor.ts +52 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/cli",
|
|
3
|
-
"version": "19.
|
|
3
|
+
"version": "19.4.0",
|
|
4
4
|
"description": "The `x` binary: new, dev, build, verify, generate, db, mcp, doctor, deploy",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,34 +37,34 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@babel/core": "^7.28.4",
|
|
40
|
-
"@ultimat3/action": "19.
|
|
41
|
-
"@ultimat3/admin": "19.
|
|
42
|
-
"@ultimat3/ai": "19.
|
|
43
|
-
"@ultimat3/auth": "19.
|
|
44
|
-
"@ultimat3/cache": "19.
|
|
45
|
-
"@ultimat3/core": "19.
|
|
46
|
-
"@ultimat3/db": "19.
|
|
47
|
-
"@ultimat3/entity": "19.
|
|
48
|
-
"@ultimat3/flags": "19.
|
|
49
|
-
"@ultimat3/http": "19.
|
|
50
|
-
"@ultimat3/i18n": "19.
|
|
51
|
-
"@ultimat3/jobs": "19.
|
|
52
|
-
"@ultimat3/mail": "19.
|
|
53
|
-
"@ultimat3/manifest": "19.
|
|
54
|
-
"@ultimat3/mcp": "19.
|
|
55
|
-
"@ultimat3/money": "19.
|
|
56
|
-
"@ultimat3/notify": "19.
|
|
57
|
-
"@ultimat3/policy": "19.
|
|
58
|
-
"@ultimat3/pwa": "19.
|
|
59
|
-
"@ultimat3/query": "19.
|
|
60
|
-
"@ultimat3/realtime": "19.
|
|
61
|
-
"@ultimat3/render": "19.
|
|
62
|
-
"@ultimat3/schema": "19.
|
|
63
|
-
"@ultimat3/scraping": "19.
|
|
64
|
-
"@ultimat3/seo": "19.
|
|
65
|
-
"@ultimat3/storage": "19.
|
|
66
|
-
"@ultimat3/testing": "19.
|
|
67
|
-
"@ultimat3/time": "19.
|
|
40
|
+
"@ultimat3/action": "19.4.0",
|
|
41
|
+
"@ultimat3/admin": "19.4.0",
|
|
42
|
+
"@ultimat3/ai": "19.4.0",
|
|
43
|
+
"@ultimat3/auth": "19.4.0",
|
|
44
|
+
"@ultimat3/cache": "19.4.0",
|
|
45
|
+
"@ultimat3/core": "19.4.0",
|
|
46
|
+
"@ultimat3/db": "19.4.0",
|
|
47
|
+
"@ultimat3/entity": "19.4.0",
|
|
48
|
+
"@ultimat3/flags": "19.4.0",
|
|
49
|
+
"@ultimat3/http": "19.4.0",
|
|
50
|
+
"@ultimat3/i18n": "19.4.0",
|
|
51
|
+
"@ultimat3/jobs": "19.4.0",
|
|
52
|
+
"@ultimat3/mail": "19.4.0",
|
|
53
|
+
"@ultimat3/manifest": "19.4.0",
|
|
54
|
+
"@ultimat3/mcp": "19.4.0",
|
|
55
|
+
"@ultimat3/money": "19.4.0",
|
|
56
|
+
"@ultimat3/notify": "19.4.0",
|
|
57
|
+
"@ultimat3/policy": "19.4.0",
|
|
58
|
+
"@ultimat3/pwa": "19.4.0",
|
|
59
|
+
"@ultimat3/query": "19.4.0",
|
|
60
|
+
"@ultimat3/realtime": "19.4.0",
|
|
61
|
+
"@ultimat3/render": "19.4.0",
|
|
62
|
+
"@ultimat3/schema": "19.4.0",
|
|
63
|
+
"@ultimat3/scraping": "19.4.0",
|
|
64
|
+
"@ultimat3/seo": "19.4.0",
|
|
65
|
+
"@ultimat3/storage": "19.4.0",
|
|
66
|
+
"@ultimat3/testing": "19.4.0",
|
|
67
|
+
"@ultimat3/time": "19.4.0",
|
|
68
68
|
"babel-preset-solid": "^1.9.15"
|
|
69
69
|
}
|
|
70
70
|
}
|
package/src/dev-roles.ts
CHANGED
|
@@ -8,7 +8,13 @@
|
|
|
8
8
|
|
|
9
9
|
import type { Role } from '@ultimat3/core';
|
|
10
10
|
import { createContext, isRole, logger, ROLES } from '@ultimat3/core';
|
|
11
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
RateLimitStore,
|
|
13
|
+
Route,
|
|
14
|
+
ServerHandle,
|
|
15
|
+
ServerHooks,
|
|
16
|
+
WebSocketMount,
|
|
17
|
+
} from '@ultimat3/http';
|
|
12
18
|
import {
|
|
13
19
|
configuredAuthenticator,
|
|
14
20
|
configuredHttp,
|
|
@@ -26,7 +32,7 @@ import {
|
|
|
26
32
|
jobDriver,
|
|
27
33
|
pgSchedulerState,
|
|
28
34
|
} from '@ultimat3/jobs';
|
|
29
|
-
import type { LiveQueryRegistry } from '@ultimat3/realtime/server';
|
|
35
|
+
import type { LiveQueryRegistry, SyncWs } from '@ultimat3/realtime/server';
|
|
30
36
|
import type { LiveReplicator } from '@ultimat3/testing';
|
|
31
37
|
import { devHooks } from './dev-hooks';
|
|
32
38
|
import type { LiveFeed } from './dev-live-feed';
|
|
@@ -36,7 +42,7 @@ import type { RunningReplicator } from './dev-replicator';
|
|
|
36
42
|
import { startReplicator } from './dev-replicator';
|
|
37
43
|
import type { RunningServices } from './dev-runtime';
|
|
38
44
|
import type { Env } from './dev-services';
|
|
39
|
-
import {
|
|
45
|
+
import { prepareSync, type RunningSync } from './dev-sync';
|
|
40
46
|
import { errorPageHook } from './error-pages';
|
|
41
47
|
import { BadFlagError, PortInvalidError, RuntimeDriverSplitError } from './errors';
|
|
42
48
|
import { DEFAULT_METRICS_PORT, startMetricsEndpoint } from './metrics-endpoint';
|
|
@@ -270,7 +276,9 @@ function rateLimitStoreFor(options: StartRolesOptions): RateLimitStore | undefin
|
|
|
270
276
|
return supplied;
|
|
271
277
|
}
|
|
272
278
|
|
|
273
|
-
|
|
279
|
+
/** `mount` is the sync node's socket, served on THIS port as well as its own — why, in `dev-sync`.
|
|
280
|
+
* Undefined without the `sync` role, and then this server opens no websocket, as it always did. */
|
|
281
|
+
function startWeb(options: StartRolesOptions, mount?: WebSocketMount<SyncWs>): ServerHandle {
|
|
274
282
|
warnIfUnauthenticatable(options.routes);
|
|
275
283
|
const binding = options.http ?? DEV_BINDING;
|
|
276
284
|
const hops = trustedHopsFromEnv(options.env);
|
|
@@ -278,6 +286,7 @@ function startWeb(options: StartRolesOptions): ServerHandle {
|
|
|
278
286
|
return createServer({
|
|
279
287
|
routes: options.routes,
|
|
280
288
|
role: 'web',
|
|
289
|
+
...(mount === undefined ? {} : { websocket: mount }),
|
|
281
290
|
hooks: devHooks({
|
|
282
291
|
...(options.devNotices === undefined ? {} : { devNotices: options.devNotices }),
|
|
283
292
|
...(options.root === undefined ? {} : { errorPage: errorPageHook(options.root) }),
|
|
@@ -373,11 +382,20 @@ export async function startRoles(options: StartRolesOptions): Promise<RunningRol
|
|
|
373
382
|
});
|
|
374
383
|
started.push(async () => metrics.stop());
|
|
375
384
|
|
|
376
|
-
|
|
385
|
+
// BUILT here, BOUND below, the web role between them: `web` serves the node's socket on its
|
|
386
|
+
// own port and a listening server cannot be handed one, while the neighbouring-port refusals
|
|
387
|
+
// are only the right answer once the web port's own has been given.
|
|
388
|
+
const prepared = selected.includes('sync') ? await prepareSync(options) : null;
|
|
389
|
+
// One rollback entry, kept current — two would stop the node twice out of a failed boot.
|
|
390
|
+
let releaseSync = prepared?.stop ?? null;
|
|
391
|
+
if (prepared !== null) started.push(async () => await releaseSync?.());
|
|
392
|
+
|
|
393
|
+
const server = selected.includes('web') ? startWeb(options, prepared?.mount) : null;
|
|
377
394
|
if (server !== null) started.push(() => server.stop());
|
|
378
395
|
|
|
379
|
-
const sync
|
|
380
|
-
|
|
396
|
+
const sync: RunningSync | null =
|
|
397
|
+
prepared === null ? null : await prepared.listen(server === null ? null : server.url());
|
|
398
|
+
if (sync !== null) releaseSync = sync.stop;
|
|
381
399
|
|
|
382
400
|
const worker = selected.includes('worker')
|
|
383
401
|
? createWorker({
|
package/src/dev-sync.ts
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
// and a listener of its own — and because that file is the boot's index, not its detail.
|
|
4
4
|
|
|
5
5
|
import { createContext, logger, UltimateError } from '@ultimat3/core';
|
|
6
|
+
import type { WebSocketMount } from '@ultimat3/http';
|
|
6
7
|
import { listQueries } from '@ultimat3/query';
|
|
8
|
+
import type { SyncNode, SyncWs } from '@ultimat3/realtime/server';
|
|
7
9
|
import {
|
|
8
10
|
ChannelHub,
|
|
9
11
|
createSyncNode,
|
|
@@ -98,6 +100,32 @@ export function syncPortFor(port: number): number {
|
|
|
98
100
|
return port + 1;
|
|
99
101
|
}
|
|
100
102
|
|
|
103
|
+
/**
|
|
104
|
+
* A node that is built and subscribed but bound to nothing yet — the one moment the `web` role can
|
|
105
|
+
* still mount its socket, since `web` binds its port first and a mount handed over after that is a
|
|
106
|
+
* server already listening without it.
|
|
107
|
+
*/
|
|
108
|
+
export interface PreparedSync {
|
|
109
|
+
readonly node: SyncNode;
|
|
110
|
+
readonly registry: LiveQueryRegistry;
|
|
111
|
+
/**
|
|
112
|
+
* The socket as the WEB role can serve it, on the port that role already publishes.
|
|
113
|
+
*
|
|
114
|
+
* `x dev` does BOTH: this mount on `PORT`, and the node's own listener on `PORT + 1` below. They
|
|
115
|
+
* are one node behind two doors, not two nodes — the sockets share the registry, the grants and
|
|
116
|
+
* the change subscription — and which door a browser uses is whichever one it can reach.
|
|
117
|
+
* `docker/` publishes the second; a laptop reached through one forwarded port uses the first.
|
|
118
|
+
*/
|
|
119
|
+
readonly mount: WebSocketMount<SyncWs>;
|
|
120
|
+
/**
|
|
121
|
+
* Bind `PORT + 1`, and answer with the same object `startSync` always did. `appUrl` is the web
|
|
122
|
+
* role's own origin when one runs here, for the line below that names both doors.
|
|
123
|
+
*/
|
|
124
|
+
listen(appUrl: string | null): Promise<RunningSync>;
|
|
125
|
+
/** Release the node when nothing ever bound it — a `web` role that threw after it was built. */
|
|
126
|
+
stop(): Promise<void>;
|
|
127
|
+
}
|
|
128
|
+
|
|
101
129
|
/** What `startRoles` holds on to: where the node listens, and how to take it down. */
|
|
102
130
|
export interface RunningSync {
|
|
103
131
|
readonly url: string;
|
|
@@ -141,13 +169,10 @@ export function registerLiveQueries(options: StartRolesOptions): LiveQueryRegist
|
|
|
141
169
|
}
|
|
142
170
|
|
|
143
171
|
/**
|
|
144
|
-
* The
|
|
145
|
-
*
|
|
146
|
-
* Port 0 is passed straight through rather than incremented — `+ 1` would ask the kernel for
|
|
147
|
-
* port 1 instead of an ephemeral one — and the reported url is the listener's own bound address,
|
|
148
|
-
* never a string built from the port that was requested.
|
|
172
|
+
* The node itself: its hub, its registry, its authenticator and its change subscription. Nothing
|
|
173
|
+
* bound — `listen()` and the `web` role's mount are the two doors, and this is what is behind both.
|
|
149
174
|
*/
|
|
150
|
-
export async function
|
|
175
|
+
export async function prepareSync(options: StartRolesOptions): Promise<PreparedSync> {
|
|
151
176
|
const sockets = new SocketRegistry();
|
|
152
177
|
const hub = new ChannelHub({ transport: options.runtime.transport, sockets });
|
|
153
178
|
// The node evaluated no credential of its own and no host ever handed it one, so every socket
|
|
@@ -180,6 +205,35 @@ export async function startSync(options: StartRolesOptions): Promise<RunningSync
|
|
|
180
205
|
}),
|
|
181
206
|
});
|
|
182
207
|
await node.start();
|
|
208
|
+
return {
|
|
209
|
+
node,
|
|
210
|
+
registry,
|
|
211
|
+
// The node's OWN path, asked rather than restated: `SyncNodeOptions.path` is settable and a
|
|
212
|
+
// second copy of `/_x/sync` here is the copy that stays behind when it moves.
|
|
213
|
+
mount: { path: node.path, fetch: node.fetch, websocket: node.websocket },
|
|
214
|
+
stop: () => node.stop(),
|
|
215
|
+
listen: async (appUrl) => await listen(options, node, registry, appUrl),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The node's own socket, on `PORT + 1`. The sync role owns it because websockets and the request
|
|
221
|
+
* pipeline drain differently, and `docker/` publishes it as a service of its own.
|
|
222
|
+
*
|
|
223
|
+
* Kept a step of its own so `web` binds BEFORE it: the two refusals below are about a taken
|
|
224
|
+
* neighbouring port, and reversing the order would answer a second `x dev` on this checkout with
|
|
225
|
+
* "port 3001 is in use" when the fact worth printing is that 3000 is.
|
|
226
|
+
*
|
|
227
|
+
* Port 0 is passed straight through rather than incremented — `+ 1` would ask the kernel for
|
|
228
|
+
* port 1 instead of an ephemeral one — and the reported url is the listener's own bound address,
|
|
229
|
+
* never a string built from the port that was requested.
|
|
230
|
+
*/
|
|
231
|
+
async function listen(
|
|
232
|
+
options: StartRolesOptions,
|
|
233
|
+
node: SyncNode,
|
|
234
|
+
registry: LiveQueryRegistry,
|
|
235
|
+
appUrl: string | null,
|
|
236
|
+
): Promise<RunningSync> {
|
|
183
237
|
const port = syncPortFor(options.port);
|
|
184
238
|
try {
|
|
185
239
|
// The SAME interface the web role binds, resolved from the same option and the same default.
|
|
@@ -192,6 +246,14 @@ export async function startSync(options: StartRolesOptions): Promise<RunningSync
|
|
|
192
246
|
// seconds of every Ctrl-C (measured 2026-09-06, 5.0s of 5.1s) spent on a reconnect frame
|
|
193
247
|
// whose target does not exist yet.
|
|
194
248
|
const listener = listenSyncNode(node, { port, hostname: binding.hostname, drainGraceMs: 0 });
|
|
249
|
+
// BOTH doors, named, once. `sync node ready` said only that a node existed: the first question
|
|
250
|
+
// a failing browser socket raises — "is the ws server up, and where?" — had no answer anywhere
|
|
251
|
+
// in the boot output, and the port was never printed at all. It is also what an editor's port
|
|
252
|
+
// forwarding reads: a url in the terminal is how VS Code and a Codespace learn a port exists.
|
|
253
|
+
logger.info('sync reachable', {
|
|
254
|
+
node: `${listener.url}${node.path}`,
|
|
255
|
+
app: appUrl === null ? null : `${appUrl}${node.path}`,
|
|
256
|
+
});
|
|
195
257
|
return {
|
|
196
258
|
url: listener.url,
|
|
197
259
|
registry,
|
|
@@ -207,3 +269,9 @@ export async function startSync(options: StartRolesOptions): Promise<RunningSync
|
|
|
207
269
|
throw error;
|
|
208
270
|
}
|
|
209
271
|
}
|
|
272
|
+
|
|
273
|
+
/** Both steps, for a caller with no web role to mount anything on. */
|
|
274
|
+
export async function startSync(options: StartRolesOptions): Promise<RunningSync> {
|
|
275
|
+
const prepared = await prepareSync(options);
|
|
276
|
+
return await prepared.listen(null);
|
|
277
|
+
}
|
package/src/verify-checks.ts
CHANGED
|
@@ -50,14 +50,21 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
50
50
|
{
|
|
51
51
|
name: 'typecheck',
|
|
52
52
|
summary: 'tsc -b across every project the root references',
|
|
53
|
+
// `typecheckBin` (`x.verify.json`, beside `agentsMdMaxBytes`) swaps the binary and nothing
|
|
54
|
+
// else: same `-b --pretty false` invocation, same output format to parse, same `X_TYPECHECK_
|
|
55
|
+
// FAILED` finding either way. Absent means `tsc` — the only checker every app already has,
|
|
56
|
+
// since `typescript` is a framework dependency and a drop-in replacement is the app's own
|
|
57
|
+
// devDependency to add, never a default this step could assume.
|
|
53
58
|
async run(ctx) {
|
|
54
|
-
const
|
|
59
|
+
const floor = await readVerifyFloor(ctx.root);
|
|
60
|
+
const bin = floor?.typecheckBin ?? 'tsc';
|
|
61
|
+
const result = await ctx.runner(['bunx', bin, '-b', '--pretty', 'false'], {
|
|
55
62
|
cwd: ctx.root,
|
|
56
63
|
});
|
|
57
64
|
return fromExec(result, {
|
|
58
65
|
code: 'X_TYPECHECK_FAILED',
|
|
59
66
|
cause: 'the project does not typecheck',
|
|
60
|
-
fix:
|
|
67
|
+
fix: `bunx ${bin} -b --pretty false`,
|
|
61
68
|
});
|
|
62
69
|
},
|
|
63
70
|
},
|
package/src/verify-floor.ts
CHANGED
|
@@ -29,6 +29,18 @@ export interface VerifyFloor {
|
|
|
29
29
|
* sees, which is the whole safeguard: the number is small, visible, and argued for in one place.
|
|
30
30
|
*/
|
|
31
31
|
readonly agentsMdMaxBytes?: number;
|
|
32
|
+
/**
|
|
33
|
+
* The binary the `typecheck` step invokes in place of `tsc` — `bunx <typecheckBin> -b --pretty
|
|
34
|
+
* false`, unchanged otherwise. Absent means `tsc`, which is the only binary every app already
|
|
35
|
+
* has: `typescript` is a framework dependency, not one this file can assume an app added.
|
|
36
|
+
*
|
|
37
|
+
* Here for the same reason `agentsMdMaxBytes` is: this is the file that configures the GATE,
|
|
38
|
+
* and it is read by the very step the key names. A drop-in `tsc -b` compatible checker (Microsoft's
|
|
39
|
+
* `tsgo`, `@typescript/native-preview`) is a devDependency + a one-line commit here, never a
|
|
40
|
+
* hardcoded default — an app that has not installed the binary this names gets `command not
|
|
41
|
+
* found` from its own shell, not a framework opinion about which compiler is correct.
|
|
42
|
+
*/
|
|
43
|
+
readonly typecheckBin?: string;
|
|
32
44
|
/** Why part of the file is not a floor. The `manifest` step reports these; nothing swallows them. */
|
|
33
45
|
readonly problems: readonly string[];
|
|
34
46
|
}
|
|
@@ -36,6 +48,9 @@ export interface VerifyFloor {
|
|
|
36
48
|
/** The floor's budget key. Named once: the problem quotes it and the fix repairs it. */
|
|
37
49
|
export const BUDGET_FIELD = 'agentsMdMaxBytes';
|
|
38
50
|
|
|
51
|
+
/** The floor's typecheck-binary key. Named once: the problem quotes it and the fix repairs it. */
|
|
52
|
+
export const TYPECHECK_BIN_FIELD = 'typecheckBin';
|
|
53
|
+
|
|
39
54
|
/**
|
|
40
55
|
* `agentsMdMaxBytes`, or a reason it is not one. A budget that is not a positive whole number is
|
|
41
56
|
* the caller's bug and must not silently fall back to the default: a floor file that says
|
|
@@ -57,6 +72,27 @@ function readBudget(payload: Record<string, unknown> | undefined): {
|
|
|
57
72
|
return { budget: raw, problems: [] };
|
|
58
73
|
}
|
|
59
74
|
|
|
75
|
+
/**
|
|
76
|
+
* `typecheckBin`, or a reason it is not one. A non-string value must not silently fall back to
|
|
77
|
+
* `tsc`: a floor that names `"typecheckBin": 7` and gets `tsc` anyway is a repository that
|
|
78
|
+
* believes the `typecheck` step is running a checker it is not, which is the same false green
|
|
79
|
+
* `readBudget` above already refuses for the byte count. An empty string is refused for the same
|
|
80
|
+
* reason `-b`'s own project argument may not be empty — `bunx '' -b …` is not a step that failed
|
|
81
|
+
* to typecheck, it is a step that never ran a compiler at all.
|
|
82
|
+
*/
|
|
83
|
+
function readTypecheckBin(payload: Record<string, unknown> | undefined): {
|
|
84
|
+
bin?: string;
|
|
85
|
+
problems: readonly string[];
|
|
86
|
+
} {
|
|
87
|
+
const raw = payload?.[TYPECHECK_BIN_FIELD];
|
|
88
|
+
if (raw === undefined) return { problems: [] };
|
|
89
|
+
if (typeof raw !== 'string' || raw.trim().length === 0)
|
|
90
|
+
return {
|
|
91
|
+
problems: [`"${TYPECHECK_BIN_FIELD}" is ${JSON.stringify(raw)}, which is not a binary name`],
|
|
92
|
+
};
|
|
93
|
+
return { bin: raw, problems: [] };
|
|
94
|
+
}
|
|
95
|
+
|
|
60
96
|
const asRecord = (value: unknown): Record<string, unknown> | undefined =>
|
|
61
97
|
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
62
98
|
? (value as Record<string, unknown>)
|
|
@@ -86,12 +122,18 @@ export function parseVerifyFloor(
|
|
|
86
122
|
}
|
|
87
123
|
const record = asRecord(payload);
|
|
88
124
|
const budget = readBudget(record);
|
|
125
|
+
const typecheckBin = readTypecheckBin(record);
|
|
89
126
|
const steps = record?.['steps'];
|
|
90
127
|
if (!Array.isArray(steps)) {
|
|
91
128
|
return {
|
|
92
129
|
steps: [],
|
|
93
130
|
...(budget.budget === undefined ? {} : { agentsMdMaxBytes: budget.budget }),
|
|
94
|
-
|
|
131
|
+
...(typecheckBin.bin === undefined ? {} : { typecheckBin: typecheckBin.bin }),
|
|
132
|
+
problems: [
|
|
133
|
+
'it has no "steps" array of step names',
|
|
134
|
+
...budget.problems,
|
|
135
|
+
...typecheckBin.problems,
|
|
136
|
+
],
|
|
95
137
|
};
|
|
96
138
|
}
|
|
97
139
|
const named = steps.filter((step): step is string => typeof step === 'string');
|
|
@@ -99,8 +141,10 @@ export function parseVerifyFloor(
|
|
|
99
141
|
return {
|
|
100
142
|
steps: named.filter((step) => declared.includes(step)),
|
|
101
143
|
...(budget.budget === undefined ? {} : { agentsMdMaxBytes: budget.budget }),
|
|
144
|
+
...(typecheckBin.bin === undefined ? {} : { typecheckBin: typecheckBin.bin }),
|
|
102
145
|
problems: [
|
|
103
146
|
...(named.length === steps.length ? [] : ['"steps" holds an entry that is not a string']),
|
|
147
|
+
...typecheckBin.problems,
|
|
104
148
|
...(unknown.length === 0
|
|
105
149
|
? []
|
|
106
150
|
: [`"steps" names ${unknown.join(', ')}, which x verify does not run`]),
|
|
@@ -185,7 +229,10 @@ export const floorProblemFindings = (floor: VerifyFloor | undefined): readonly F
|
|
|
185
229
|
* does not fix it is the failure `packages/cli/CLAUDE.md` names, and the budget is the first
|
|
186
230
|
* problem this file can report that is not about `steps` at all.
|
|
187
231
|
*/
|
|
188
|
-
const fixFor = (problem: string): string =>
|
|
189
|
-
problem.includes(`"${BUDGET_FIELD}"`)
|
|
190
|
-
|
|
191
|
-
|
|
232
|
+
const fixFor = (problem: string): string => {
|
|
233
|
+
if (problem.includes(`"${BUDGET_FIELD}"`))
|
|
234
|
+
return `x verify --json # then set "${BUDGET_FIELD}" in ${VERIFY_FLOOR_FILE} to a positive whole number of bytes, or drop the key for the ${AGENTS_MD_MAX_BYTES}B default`;
|
|
235
|
+
if (problem.includes(`"${TYPECHECK_BIN_FIELD}"`))
|
|
236
|
+
return `x verify --json # then set "${TYPECHECK_BIN_FIELD}" in ${VERIFY_FLOOR_FILE} to a non-empty binary name, or drop the key to run tsc`;
|
|
237
|
+
return `x verify --json # then write ${VERIFY_FLOOR_FILE} as {"steps":["unit","contract"]}, naming only steps it ran`;
|
|
238
|
+
};
|