@vzn/vx 0.0.652 → 0.0.653
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/README.md +2 -2
- package/launcher.sh +2 -1
- package/package.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/show.json +1 -0
- package/skills/vx/SKILL.md +3 -3
- package/src/bin.ts +25 -3
- package/src/cache/archive.ts +196 -31
- package/src/cache/cache.ts +863 -225
- package/src/cache/checkpointer.ts +102 -0
- package/src/cache/file-hashes.ts +2 -1
- package/src/cache/fold-memo.ts +24 -4
- package/src/cache/fs-clock.ts +26 -0
- package/src/cache/git-inputs.ts +188 -22
- package/src/cache/history-stream.ts +22 -5
- package/src/cache/index.ts +10 -1
- package/src/cache/inputs.ts +573 -152
- package/src/cache/key-fold.ts +1 -1
- package/src/cache/layer.ts +23 -10
- package/src/cache/layered-cache.ts +28 -7
- package/src/cache/output-index.ts +106 -44
- package/src/cache/plugin-cache.ts +2 -2
- package/src/cache/policy.ts +6 -2
- package/src/cache/remote-lane.ts +3 -1
- package/src/cache/run-history.ts +50 -10
- package/src/cache/schema.ts +42 -9
- package/src/cli/cache.ts +147 -34
- package/src/cli/completions.ts +2 -2
- package/src/cli/core-alias.ts +9 -2
- package/src/cli/docs.ts +4 -2
- package/src/cli/help.ts +25 -2
- package/src/cli/index.ts +38 -7
- package/src/cli/info.ts +4 -3
- package/src/cli/init.ts +55 -20
- package/src/cli/last.ts +184 -41
- package/src/cli/lock.ts +187 -24
- package/src/cli/plan-format.ts +24 -21
- package/src/cli/plugin-commands.ts +6 -1
- package/src/cli/plugin-templates.ts +1 -1
- package/src/cli/run.ts +68 -34
- package/src/cli/select.ts +160 -64
- package/src/cli/show.ts +63 -18
- package/src/cli/task-verb.ts +12 -7
- package/src/cli/upgrade.ts +210 -76
- package/src/cli/watch-filter.ts +88 -15
- package/src/cli/watch-fs.ts +34 -47
- package/src/cli/watch-judge.ts +72 -8
- package/src/cli/watch-set.ts +6 -4
- package/src/cli/watch.ts +179 -25
- package/src/cli/why.ts +107 -33
- package/src/cli/workspace-config.ts +11 -1
- package/src/config.ts +9 -0
- package/src/exec/env.ts +3 -1
- package/src/exec/executor.ts +76 -15
- package/src/exec/kill-tree.ts +7 -7
- package/src/exec/runner.ts +270 -108
- package/src/exec/sandbox-binds.ts +109 -18
- package/src/exec/sandbox-runtime.ts +134 -37
- package/src/exec/sandbox-violations.ts +250 -46
- package/src/exec/spawn-off.ts +10 -5
- package/src/exec/warm-shell.ts +29 -7
- package/src/graph/folds.ts +89 -16
- package/src/graph/index.ts +8 -0
- package/src/graph/priorities.ts +8 -5
- package/src/graph/scheduler.ts +173 -38
- package/src/graph/stand-ins.ts +36 -0
- package/src/graph/task-graph.ts +327 -141
- package/src/graph/task-status.ts +5 -1
- package/src/handoff.ts +45 -10
- package/src/history/failure-mode.ts +15 -5
- package/src/history/history.ts +103 -30
- package/src/history/index.ts +1 -0
- package/src/history/metrics.ts +189 -69
- package/src/index.ts +2 -1
- package/src/orchestrator/admission.ts +13 -9
- package/src/orchestrator/affected-tasks.ts +35 -13
- package/src/orchestrator/command-windows.ts +8 -5
- package/src/orchestrator/deferred-outputs.ts +41 -12
- package/src/orchestrator/doctor.ts +21 -9
- package/src/orchestrator/download-policy.ts +3 -3
- package/src/orchestrator/excluded-keys.ts +14 -2
- package/src/orchestrator/execute-args.ts +9 -2
- package/src/orchestrator/execute-task.ts +564 -123
- package/src/orchestrator/graph-memo.ts +26 -4
- package/src/orchestrator/graph-stage.ts +27 -4
- package/src/orchestrator/held-servers.ts +36 -14
- package/src/orchestrator/hit-restore.ts +136 -87
- package/src/orchestrator/index.ts +4 -1
- package/src/orchestrator/keyed-projects.ts +25 -0
- package/src/orchestrator/local-floor.ts +21 -3
- package/src/orchestrator/lockfile-claim.ts +30 -4
- package/src/orchestrator/miss-save.ts +33 -53
- package/src/orchestrator/options.ts +13 -0
- package/src/orchestrator/output-host.ts +16 -4
- package/src/orchestrator/output-log.ts +1 -54
- package/src/orchestrator/persistent-task.ts +31 -17
- package/src/orchestrator/persistent.ts +152 -15
- package/src/orchestrator/placement.ts +81 -15
- package/src/orchestrator/plan.ts +37 -15
- package/src/orchestrator/plugin-host.ts +232 -108
- package/src/orchestrator/plugin.ts +53 -27
- package/src/orchestrator/prepare.ts +194 -38
- package/src/orchestrator/projects.ts +89 -10
- package/src/orchestrator/read-only.ts +115 -0
- package/src/orchestrator/run-context.ts +6 -2
- package/src/orchestrator/run-failures.ts +43 -14
- package/src/orchestrator/run-id.ts +13 -2
- package/src/orchestrator/run-keys.ts +87 -28
- package/src/orchestrator/run-live.ts +16 -13
- package/src/orchestrator/run-lock.ts +27 -4
- package/src/orchestrator/run-records.ts +12 -10
- package/src/orchestrator/run-report.ts +2 -1
- package/src/orchestrator/run-stop.ts +17 -9
- package/src/orchestrator/run.ts +443 -107
- package/src/orchestrator/sandbox-fix.ts +40 -12
- package/src/orchestrator/sandbox-request.ts +286 -59
- package/src/orchestrator/save-lane.ts +24 -13
- package/src/orchestrator/shell-verdict.ts +20 -1
- package/src/orchestrator/signals.ts +103 -8
- package/src/orchestrator/stable-keys.ts +17 -9
- package/src/orchestrator/task-artifacts.ts +15 -2
- package/src/orchestrator/task-hash.ts +187 -30
- package/src/orchestrator/telemetry-host.ts +25 -6
- package/src/orchestrator/telemetry.ts +80 -12
- package/src/orchestrator/upstream.ts +3 -1
- package/src/report/events.ts +20 -3
- package/src/report/forecast.ts +103 -37
- package/src/report/framed-output.ts +165 -157
- package/src/report/index.ts +2 -1
- package/src/report/logger.ts +107 -30
- package/src/report/plain-output.ts +13 -9
- package/src/report/status-line.ts +28 -55
- package/src/report/summary.ts +57 -14
- package/src/settings.ts +42 -6
- package/src/ui/components.ts +36 -42
- package/src/ui/index.ts +3 -1
- package/src/ui/live-region.ts +67 -8
- package/src/ui/terminal.ts +12 -0
- package/src/ui/theme.ts +16 -1
- package/src/util/config-frame.ts +17 -3
- package/src/util/edit-distance.ts +17 -1
- package/src/util/errors.ts +23 -7
- package/src/util/foreign-cache-dir.ts +60 -0
- package/src/util/git-ignored.ts +59 -5
- package/src/util/index.ts +15 -1
- package/src/util/paths.ts +50 -6
- package/src/util/plain-text.ts +19 -0
- package/src/util/secret-mask.ts +57 -1
- package/src/util/settle.ts +16 -0
- package/src/util/size.ts +12 -2
- package/src/util/write-file.ts +86 -9
- package/src/workspace/affected.ts +225 -27
- package/src/workspace/config-bytecode.ts +23 -4
- package/src/workspace/config-eval.ts +90 -16
- package/src/workspace/config-imports.ts +96 -41
- package/src/workspace/config-schema.ts +126 -25
- package/src/workspace/dependencies-lock.ts +7 -14
- package/src/workspace/filter.ts +10 -1
- package/src/workspace/index.ts +3 -1
- package/src/workspace/json-data.ts +34 -8
- package/src/workspace/lockfile.ts +86 -37
- package/src/workspace/migration.ts +46 -25
- package/src/workspace/module-state.ts +62 -0
- package/src/workspace/nested-dirs.ts +8 -0
- package/src/workspace/project-loader.ts +276 -32
- package/src/workspace/repo-id.ts +29 -6
- package/src/workspace/workspace.ts +190 -30
|
@@ -46,6 +46,7 @@ import type {
|
|
|
46
46
|
} from './plugin.js'
|
|
47
47
|
import type { ProjectMeta } from '../workspace/index.js'
|
|
48
48
|
import { hashExtra } from './lockfile-claim.js'
|
|
49
|
+
import { readOnly, readOnlyNodes } from './read-only.js'
|
|
49
50
|
|
|
50
51
|
/** `a string`, `an array`, `null`, `a number` — for a refusal that names what came back. */
|
|
51
52
|
function describeValue(v: unknown): string {
|
|
@@ -99,18 +100,61 @@ async function safe<T>(plugin: VxPlugin, hook: string, fn: () => T | Promise<T>)
|
|
|
99
100
|
try {
|
|
100
101
|
return await fn()
|
|
101
102
|
} catch (err) {
|
|
102
|
-
|
|
103
|
-
// a plugin's verb print it as they get it, and the throw quotes what the
|
|
104
|
-
// plugin was told (a remote's reply, a header) (L-11).
|
|
105
|
-
// A plugin's own code survives (cli.md § Error codes): an agent
|
|
106
|
-
// branches on it.
|
|
107
|
-
throw new UserError(
|
|
108
|
-
maskedLine(`plugin '${plugin.name}' failed in ${hook}: ${thrownMessage(err)}`),
|
|
109
|
-
errorCode(err) === 'VX_E_INTERNAL' ? 'VX_E_REFUSED' : errorCode(err),
|
|
110
|
-
)
|
|
103
|
+
throw pluginFailure(plugin, hook, err)
|
|
111
104
|
}
|
|
112
105
|
}
|
|
113
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Reads and checks what a hook answered, in the plugin's name as `safe`
|
|
109
|
+
* runs the hook: a getter or Proxy trap in the answer that threw crashed
|
|
110
|
+
* vx with a raw stack. Core's own refusals pass as they are. What `read`
|
|
111
|
+
* returns is what the caller uses, so a getter cannot answer the check one
|
|
112
|
+
* value and the run another.
|
|
113
|
+
*/
|
|
114
|
+
function reading<T>(plugin: VxPlugin, hook: string, read: () => T): T {
|
|
115
|
+
try {
|
|
116
|
+
return read()
|
|
117
|
+
} catch (err) {
|
|
118
|
+
if (err instanceof UserError) throw err
|
|
119
|
+
throw pluginFailure(plugin, hook, err)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function pluginFailure(plugin: VxPlugin, hook: string, err: unknown): UserError {
|
|
124
|
+
// Masked here, not by whoever prints it: an embedder of `run()` and
|
|
125
|
+
// a plugin's verb print it as they get it, and the throw quotes what the
|
|
126
|
+
// plugin was told (a remote's reply, a header) (L-11).
|
|
127
|
+
// A plugin's own code survives (cli.md § Error codes): an agent
|
|
128
|
+
// branches on it.
|
|
129
|
+
// A plugin's bug (a TypeError, not a message it chose to throw) names
|
|
130
|
+
// where it threw: the message alone left its author no file or line.
|
|
131
|
+
const at = isBug(err) ? pluginThrowSite(err) : undefined
|
|
132
|
+
return new UserError(
|
|
133
|
+
maskedLine(
|
|
134
|
+
`plugin '${plugin.name}' failed in ${hook}: ${thrownMessage(err)}${at === undefined ? '' : ` (at ${at})`}`,
|
|
135
|
+
),
|
|
136
|
+
errorCode(err) === 'VX_E_INTERNAL' ? 'VX_E_REFUSED' : errorCode(err),
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const VX_SRC = `${path.resolve(import.meta.dir, '..')}${path.sep}`
|
|
141
|
+
|
|
142
|
+
const isBug = (err: unknown): err is Error =>
|
|
143
|
+
err instanceof TypeError ||
|
|
144
|
+
err instanceof ReferenceError ||
|
|
145
|
+
err instanceof RangeError ||
|
|
146
|
+
err instanceof SyntaxError
|
|
147
|
+
|
|
148
|
+
/** The first stack frame outside vx's own source, relative to the cwd when under it. */
|
|
149
|
+
function pluginThrowSite(err: Error): string | undefined {
|
|
150
|
+
const site = [...String(err.stack ?? '').matchAll(/^\s+at (?:.*\()?(\/[^()\n]+:\d+:\d+)\)?$/gm)]
|
|
151
|
+
.map((m) => m[1]!)
|
|
152
|
+
.find((s) => !s.startsWith(VX_SRC))
|
|
153
|
+
if (site === undefined) return undefined
|
|
154
|
+
const rel = path.relative(process.cwd(), site)
|
|
155
|
+
return rel.startsWith('..') ? site : rel
|
|
156
|
+
}
|
|
157
|
+
|
|
114
158
|
// --- Pipeline stages ---------------------------------------------------------
|
|
115
159
|
//
|
|
116
160
|
// Each stage hands plugins the object core is about to use, in declaration
|
|
@@ -156,7 +200,9 @@ export async function applyConfigHooks(
|
|
|
156
200
|
for (const plugin of plugins) {
|
|
157
201
|
if (plugin.config === undefined) continue
|
|
158
202
|
const returned = await safe(plugin, 'config', () => plugin.config!(workspace, ctx))
|
|
159
|
-
|
|
203
|
+
reading(plugin, 'config', () =>
|
|
204
|
+
assertEditedInPlace(plugin, 'config', returned, workspace, 'the workspace config'),
|
|
205
|
+
)
|
|
160
206
|
afterEach?.(plugin)
|
|
161
207
|
}
|
|
162
208
|
}
|
|
@@ -180,13 +226,18 @@ export async function applyDiscoverHooks(
|
|
|
180
226
|
const edges: DiscoveredEdges[] = []
|
|
181
227
|
for (const plugin of plugins) {
|
|
182
228
|
if (plugin.discover === undefined) continue
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
|
|
229
|
+
const answer: unknown = await safe(plugin, 'discover', () =>
|
|
230
|
+
plugin.discover!({ ...ctx, projects: readOnly(projects) }),
|
|
231
|
+
)
|
|
232
|
+
const named = reading(plugin, 'discover', () => {
|
|
233
|
+
if (!Array.isArray(answer)) {
|
|
234
|
+
throw new UserError(
|
|
235
|
+
`plugin '${plugin.name}' failed in discover: expected an array, got ${describeValue(answer)}`,
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
return Array.from(answer as unknown[], plainEntry)
|
|
239
|
+
})
|
|
240
|
+
for (const entry of named) {
|
|
190
241
|
const { meta, added } = await add(entry, plugin)
|
|
191
242
|
if (added) projects.push(meta)
|
|
192
243
|
if (entry.deps !== undefined || entry.reach !== undefined) {
|
|
@@ -197,6 +248,22 @@ export async function applyDiscoverHooks(
|
|
|
197
248
|
return edges
|
|
198
249
|
}
|
|
199
250
|
|
|
251
|
+
/**
|
|
252
|
+
* A `discover` entry's fields, each read once: deps that read as a list
|
|
253
|
+
* to the check and as 5 after it crashed the edge link. `add` checks the
|
|
254
|
+
* copy's shape.
|
|
255
|
+
*/
|
|
256
|
+
function plainEntry(entry: unknown): DiscoveredProject {
|
|
257
|
+
if (typeof entry !== 'object' || entry === null) return entry as DiscoveredProject
|
|
258
|
+
const { dir, name, deps, reach } = entry as Record<string, unknown>
|
|
259
|
+
return {
|
|
260
|
+
dir,
|
|
261
|
+
name,
|
|
262
|
+
deps: Array.isArray(deps) ? [...(deps as unknown[])] : deps,
|
|
263
|
+
reach: Array.isArray(reach) ? [...(reach as unknown[])] : reach,
|
|
264
|
+
} as DiscoveredProject
|
|
265
|
+
}
|
|
266
|
+
|
|
200
267
|
export interface DiscoveredEdges {
|
|
201
268
|
readonly by: string
|
|
202
269
|
readonly meta: ProjectMeta
|
|
@@ -216,7 +283,9 @@ export async function applyProjectHooks(
|
|
|
216
283
|
for (const plugin of plugins) {
|
|
217
284
|
if (plugin.project === undefined) continue
|
|
218
285
|
const returned = await safe(plugin, 'project', () => plugin.project!(config, ctx))
|
|
219
|
-
|
|
286
|
+
reading(plugin, 'project', () =>
|
|
287
|
+
assertEditedInPlace(plugin, 'project', returned, config, "the project's config"),
|
|
288
|
+
)
|
|
220
289
|
afterEach?.(plugin)
|
|
221
290
|
}
|
|
222
291
|
}
|
|
@@ -244,7 +313,9 @@ export async function applyGraphHooks(
|
|
|
244
313
|
for (const plugin of plugins) {
|
|
245
314
|
if (plugin.graph === undefined) continue
|
|
246
315
|
const returned = await safe(plugin, 'graph', () => plugin.graph!(nodes, ctx))
|
|
247
|
-
|
|
316
|
+
reading(plugin, 'graph', () =>
|
|
317
|
+
assertEditedInPlace(plugin, 'graph', returned, nodes, 'the task graph'),
|
|
318
|
+
)
|
|
248
319
|
await safe(plugin, 'graph', () => checkNodeShapes(nodes))
|
|
249
320
|
afterEach?.(plugin)
|
|
250
321
|
last = plugin
|
|
@@ -310,21 +381,25 @@ export async function applyKeyHooks(
|
|
|
310
381
|
): Promise<void> {
|
|
311
382
|
for (const node of nodes.values()) {
|
|
312
383
|
const parts: Array<readonly [string, string]> = []
|
|
384
|
+
const task = readOnly(node)
|
|
313
385
|
for (const plugin of plugins) {
|
|
314
386
|
if (plugin.key === undefined) continue
|
|
315
|
-
const material = await safe(plugin, 'key', () => plugin.key!(
|
|
387
|
+
const material = await safe(plugin, 'key', () => plugin.key!(task, ctx))
|
|
316
388
|
if (material === undefined) continue
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
389
|
+
const entries = reading(plugin, 'key', () => {
|
|
390
|
+
// `Object.entries` over a string yields its characters as string
|
|
391
|
+
// values, so a plugin returning `'v22'` used to fold parts named
|
|
392
|
+
// '0', '1', '2' into every key — silently, and permanently. A Map or a
|
|
393
|
+
// class instance keeps its data off own enumerable keys, so
|
|
394
|
+
// `Object.entries` read it as no material at all: a silent miskey.
|
|
395
|
+
if (!isRecord(material)) {
|
|
396
|
+
throw new UserError(
|
|
397
|
+
`plugin '${plugin.name}' failed in key: returned ${describeValue(material)}, not a record of string values`,
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
return Object.entries(material)
|
|
401
|
+
})
|
|
402
|
+
for (const [name, value] of entries) {
|
|
328
403
|
if (typeof value !== 'string') {
|
|
329
404
|
throw new UserError(
|
|
330
405
|
`plugin '${plugin.name}' failed in key: value for '${name}' on ${node.id} is not a string`,
|
|
@@ -421,19 +496,30 @@ export async function claimedDigests(
|
|
|
421
496
|
digests.call(plugin.fingerprint, file, ctx),
|
|
422
497
|
)
|
|
423
498
|
if (answer === undefined) return undefined
|
|
424
|
-
const { hash, projects, extras } = (
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
(extras
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
499
|
+
const { hash, projects, extras } = reading(plugin, 'fingerprint', () => {
|
|
500
|
+
const read = (answer ?? {}) as Partial<Record<keyof FingerprintDigests, unknown>>
|
|
501
|
+
const hash = read.hash
|
|
502
|
+
const projects = read.projects
|
|
503
|
+
const digests =
|
|
504
|
+
typeof projects === 'object' && projects !== null ? Object.entries(projects) : undefined
|
|
505
|
+
const extras = Array.isArray(read.extras) ? [...(read.extras as unknown[])] : read.extras
|
|
506
|
+
if (
|
|
507
|
+
typeof hash !== 'string' ||
|
|
508
|
+
digests === undefined ||
|
|
509
|
+
digests.some(([, d]) => typeof d !== 'string') ||
|
|
510
|
+
(extras !== undefined &&
|
|
511
|
+
(!Array.isArray(extras) || extras.some((rel) => typeof rel !== 'string')))
|
|
512
|
+
) {
|
|
513
|
+
throw new UserError(
|
|
514
|
+
`plugin '${plugin.name}' failed in fingerprint: digests returned ${describeValue(answer)}, not { hash, projects: { name: digest }, extras?: [path] }`,
|
|
515
|
+
)
|
|
516
|
+
}
|
|
517
|
+
return {
|
|
518
|
+
hash,
|
|
519
|
+
projects: Object.fromEntries(digests) as FingerprintDigests['projects'],
|
|
520
|
+
extras: extras as string[] | undefined,
|
|
521
|
+
}
|
|
522
|
+
})
|
|
437
523
|
const known = new Set(ctx.projects.map((p) => p.name))
|
|
438
524
|
const stray = Object.keys(projects).find((n) => !known.has(n))
|
|
439
525
|
if (stray !== undefined) {
|
|
@@ -454,24 +540,26 @@ async function projectNames(
|
|
|
454
540
|
ctx: FingerprintContext,
|
|
455
541
|
): Promise<Set<string> | undefined> {
|
|
456
542
|
if (answer === undefined) return undefined
|
|
457
|
-
// A
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
543
|
+
// A lazy list (a generator) runs the plugin's code as it is read.
|
|
544
|
+
const read = reading(plugin, 'fingerprint', () => {
|
|
545
|
+
// A plain object is no list: `for…of` threw "is not iterable", naming
|
|
546
|
+
// neither the plugin nor the hook.
|
|
547
|
+
if (
|
|
548
|
+
typeof answer === 'string' ||
|
|
549
|
+
typeof answer !== 'object' ||
|
|
550
|
+
answer === null ||
|
|
551
|
+
typeof (answer as Partial<Iterable<unknown>>)[Symbol.iterator] !== 'function'
|
|
552
|
+
) {
|
|
553
|
+
throw new UserError(
|
|
554
|
+
`plugin '${plugin.name}' failed in fingerprint: returned ${describeValue(answer)}, not a list of project names`,
|
|
555
|
+
)
|
|
556
|
+
}
|
|
557
|
+
return [...(answer as Iterable<unknown>)]
|
|
558
|
+
})
|
|
469
559
|
const names = new Set<string>()
|
|
470
560
|
// A name no project carries selected nothing, and the tasks it meant to
|
|
471
561
|
// select did not run, with no word said.
|
|
472
562
|
const known = new Set(ctx.projects.map((p) => p.name))
|
|
473
|
-
// A lazy list (a generator) runs the plugin's code as it is read.
|
|
474
|
-
const read = await safe(plugin, 'fingerprint', () => [...(answer as Iterable<unknown>)])
|
|
475
563
|
for (const name of read) {
|
|
476
564
|
if (typeof name !== 'string') {
|
|
477
565
|
throw new UserError(
|
|
@@ -545,6 +633,9 @@ export async function applyScheduleHooks(
|
|
|
545
633
|
const refining = async (): Promise<void> => {
|
|
546
634
|
while (promised.size > 0) await Promise.all(promised)
|
|
547
635
|
}
|
|
636
|
+
// The keys are derived by now: a node a hook edited ran under the key of
|
|
637
|
+
// the one declared.
|
|
638
|
+
const viewed = readOnlyNodes(nodes)
|
|
548
639
|
const merged = (): ReadonlyMap<string, number> => {
|
|
549
640
|
const out = new Map<string, number>()
|
|
550
641
|
for (const set of sets) if (set !== undefined) for (const [id, w] of set) out.set(id, w)
|
|
@@ -591,12 +682,13 @@ export async function applyScheduleHooks(
|
|
|
591
682
|
const flaky = (taskIds: Iterable<string>): void => run.flaky?.(taskIds)
|
|
592
683
|
const own = { ...ctx, cache: cacheFor(plugin.name), refine, onRefine, refining, flaky }
|
|
593
684
|
const call = (): Promise<ReadonlyMap<string, number> | undefined> =>
|
|
594
|
-
safe(plugin, 'schedule', () => plugin.schedule!(
|
|
685
|
+
safe(plugin, 'schedule', () => plugin.schedule!(viewed, own))
|
|
595
686
|
const weights = await (run.step === undefined
|
|
596
687
|
? call()
|
|
597
688
|
: run.step(`${plugin.name}:schedule`, call))
|
|
598
689
|
// A refine that landed while the call ran is the newer set.
|
|
599
|
-
if (weights !== undefined)
|
|
690
|
+
if (weights !== undefined)
|
|
691
|
+
sets[i] ??= reading(plugin, 'schedule', () => scheduleWeights(plugin, weights, nodes))
|
|
600
692
|
}
|
|
601
693
|
return merged()
|
|
602
694
|
}
|
|
@@ -653,9 +745,13 @@ export function buildAdmission(
|
|
|
653
745
|
// The scheduler asks only about ids in this same map, so the lookup
|
|
654
746
|
// always lands: an unknown-id arm here was deleted with the whole core
|
|
655
747
|
// suite green (item 651).
|
|
656
|
-
|
|
748
|
+
// Read-only: the key is derived, and a hook's edit ran under it.
|
|
749
|
+
const task = readOnly(nodes.get(id)!)
|
|
657
750
|
const ctx = {
|
|
658
|
-
running: [...running].flatMap((r) =>
|
|
751
|
+
running: [...running].flatMap((r) => {
|
|
752
|
+
const node = nodes.get(r)
|
|
753
|
+
return node === undefined ? [] : [readOnly(node)]
|
|
754
|
+
}),
|
|
659
755
|
concurrency,
|
|
660
756
|
}
|
|
661
757
|
for (const plugin of answering) {
|
|
@@ -726,18 +822,20 @@ export async function resolveCache(
|
|
|
726
822
|
lentBy === undefined ? base : { ...base, lent: () => lentBy()?.[plugin.name] }
|
|
727
823
|
const remote = await safe(plugin, 'cache', () => plugin.cache!(ctx))
|
|
728
824
|
if (remote === undefined) continue
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
if (odd.length > 0) {
|
|
736
|
-
throw new UserError(
|
|
737
|
-
`plugin '${plugin.name}' returned from cache something that is not a remote cache layer: ` +
|
|
738
|
-
`${odd.join(', ')} ${odd.length === 1 ? 'not a function' : 'not functions'}`,
|
|
825
|
+
reading(plugin, 'cache', () => {
|
|
826
|
+
assertShape(plugin, 'cache', remote, REMOTE_CACHE_METHODS, 'a remote cache layer')
|
|
827
|
+
// `hasMany: true` failed every batch probe, and `missing: true` every
|
|
828
|
+
// upload, each said per call as the remote's own fault.
|
|
829
|
+
const odd = OPTIONAL_REMOTE_METHODS.filter(
|
|
830
|
+
(m) => remote[m] !== undefined && typeof remote[m] !== 'function',
|
|
739
831
|
)
|
|
740
|
-
|
|
832
|
+
if (odd.length > 0) {
|
|
833
|
+
throw new UserError(
|
|
834
|
+
`plugin '${plugin.name}' returned from cache something that is not a remote cache layer: ` +
|
|
835
|
+
`${odd.join(', ')} ${odd.length === 1 ? 'not a function' : 'not functions'}`,
|
|
836
|
+
)
|
|
837
|
+
}
|
|
838
|
+
})
|
|
741
839
|
const entry: ResolvedRemote = {
|
|
742
840
|
remote,
|
|
743
841
|
plugin: plugin.name,
|
|
@@ -894,6 +992,7 @@ const EXECUTOR_HOOKS = [
|
|
|
894
992
|
'advice',
|
|
895
993
|
'profile',
|
|
896
994
|
'running',
|
|
995
|
+
'unneeded',
|
|
897
996
|
] as const
|
|
898
997
|
|
|
899
998
|
/**
|
|
@@ -911,46 +1010,71 @@ export async function resolveExecutors(
|
|
|
911
1010
|
if (plugin.executor === undefined) continue
|
|
912
1011
|
const executor = await safe(plugin, 'executor', () => plugin.executor!(ctx))
|
|
913
1012
|
if (executor === undefined) continue
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
`plugin '${plugin.name}' returned executor '${executor.name}' with capacity ${typeof capacity === 'number' ? String(capacity) : JSON.stringify(capacity)}: it must be a positive integer`,
|
|
923
|
-
)
|
|
924
|
-
}
|
|
925
|
-
// Read as `=== true`: `remote: 'yes'` ran a remote executor as a local one.
|
|
926
|
-
for (const flag of EXECUTOR_FLAGS) {
|
|
927
|
-
const v: unknown = executor[flag]
|
|
928
|
-
if (v !== undefined && typeof v !== 'boolean') {
|
|
1013
|
+
reading(plugin, 'executor', () => {
|
|
1014
|
+
assertShape(plugin, 'executor', executor, ['execute'], 'an executor')
|
|
1015
|
+
if (typeof executor.name !== 'string' || executor.name.length === 0) {
|
|
1016
|
+
throw new UserError(`plugin '${plugin.name}' returned an executor with no name`)
|
|
1017
|
+
}
|
|
1018
|
+
// The pool's room is `active < capacity`: 0 or NaN parks its tasks for good.
|
|
1019
|
+
const capacity: unknown = executor.capacity
|
|
1020
|
+
if (capacity !== undefined && !(Number.isInteger(capacity) && (capacity as number) > 0)) {
|
|
929
1021
|
throw new UserError(
|
|
930
|
-
`plugin '${plugin.name}' returned executor '${executor.name}' with
|
|
1022
|
+
`plugin '${plugin.name}' returned executor '${executor.name}' with capacity ${typeof capacity === 'number' ? String(capacity) : JSON.stringify(capacity)}: it must be a positive integer`,
|
|
931
1023
|
)
|
|
932
1024
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1025
|
+
// Read as `=== true`: `remote: 'yes'` ran a remote executor as a local one.
|
|
1026
|
+
for (const flag of EXECUTOR_FLAGS) {
|
|
1027
|
+
const v: unknown = executor[flag]
|
|
1028
|
+
if (v !== undefined && typeof v !== 'boolean') {
|
|
1029
|
+
throw new UserError(
|
|
1030
|
+
`plugin '${plugin.name}' returned executor '${executor.name}' with ${flag} ${typeof v === 'string' ? JSON.stringify(v) : typeof v === 'object' && v !== null ? describeValue(v) : String(v as number)}: it must be true or false`,
|
|
1031
|
+
)
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
const platforms: unknown = executor.platforms
|
|
1035
|
+
if (
|
|
1036
|
+
platforms !== undefined &&
|
|
1037
|
+
!(
|
|
1038
|
+
Array.isArray(platforms) &&
|
|
1039
|
+
platforms.length > 0 &&
|
|
1040
|
+
platforms.every((p) => (PLATFORMS as readonly unknown[]).includes(p))
|
|
1041
|
+
)
|
|
1042
|
+
) {
|
|
950
1043
|
throw new UserError(
|
|
951
|
-
`plugin '${plugin.name}' returned executor '${executor.name}' with ${
|
|
1044
|
+
`plugin '${plugin.name}' returned executor '${executor.name}' with platforms ${JSON.stringify(platforms)}: it must be a non-empty array of ${PLATFORMS.join(', ')}`,
|
|
952
1045
|
)
|
|
953
|
-
|
|
1046
|
+
}
|
|
1047
|
+
const workers: unknown = executor.workers
|
|
1048
|
+
if (workers !== undefined) {
|
|
1049
|
+
const offered = (platforms ?? []) as readonly unknown[]
|
|
1050
|
+
const names = new Set<unknown>()
|
|
1051
|
+
const ok =
|
|
1052
|
+
Array.isArray(workers) &&
|
|
1053
|
+
workers.length > 0 &&
|
|
1054
|
+
workers.every(
|
|
1055
|
+
(w: unknown) =>
|
|
1056
|
+
Array.isArray(w) &&
|
|
1057
|
+
typeof w[0] === 'string' &&
|
|
1058
|
+
w[0].length > 0 &&
|
|
1059
|
+
!names.has(w[0]) &&
|
|
1060
|
+
names.add(w[0]) !== undefined &&
|
|
1061
|
+
Array.isArray(w[1]) &&
|
|
1062
|
+
w[1].length > 0 &&
|
|
1063
|
+
(w[1] as unknown[]).every((p) => offered.includes(p)),
|
|
1064
|
+
)
|
|
1065
|
+
if (!ok)
|
|
1066
|
+
throw new UserError(
|
|
1067
|
+
`plugin '${plugin.name}' returned executor '${executor.name}' with workers ${JSON.stringify(workers)}: it must be a non-empty array of distinct [name, platforms] pairs, each platform one of its platforms`,
|
|
1068
|
+
)
|
|
1069
|
+
}
|
|
1070
|
+
for (const hook of EXECUTOR_HOOKS) {
|
|
1071
|
+
const v: unknown = executor[hook]
|
|
1072
|
+
if (v !== undefined && typeof v !== 'function')
|
|
1073
|
+
throw new UserError(
|
|
1074
|
+
`plugin '${plugin.name}' returned executor '${executor.name}' with ${hook} that is not a function`,
|
|
1075
|
+
)
|
|
1076
|
+
}
|
|
1077
|
+
})
|
|
954
1078
|
executorPlugin.set(executor, plugin.name)
|
|
955
1079
|
executors.push(executor)
|
|
956
1080
|
}
|