rvlib-shipkit 1.3.0 → 1.5.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/CHANGELOG.md +105 -0
- package/README.md +17 -17
- package/package.json +3 -2
- package/src/asks/asks-queue.ts +4 -2
- package/src/assets/kits/identity-kit.ts +15 -13
- package/src/assets/run-assets.ts +12 -2
- package/src/check.ts +73 -98
- package/src/checks/SK001-package-json-shape.check.ts +26 -4
- package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
- package/src/checks/SK003-claude-md-shape.check.ts +5 -1
- package/src/checks/SK004-gitignore-policy.check.ts +7 -4
- package/src/checks/SK005-rvlib-versions.check.ts +53 -21
- package/src/checks/SK006-npm-publish-safety.check.ts +4 -0
- package/src/checks/SK007-github-visibility.check.ts +4 -2
- package/src/checks/SK008-reviewer-agent.check.ts +6 -4
- package/src/checks/SK009-commit-ai-tells.check.ts +94 -74
- package/src/checks/SK010-undeclared-unit.check.ts +4 -0
- package/src/checks/SK011-sibling-unit-ranges.check.ts +4 -0
- package/src/checks/SK012-config-barrel-import.check.ts +5 -1
- package/src/checks/SK013-one-way-in-policy.check.ts +7 -3
- package/src/checks/SK014-prose-ste.check.ts +20 -6
- package/src/checks/SK015-nested-repo.check.ts +4 -2
- package/src/checks/SK016-legacy-check-shapes.check.ts +23 -16
- package/src/checks/SK017-vscode-hub-task.check.ts +22 -7
- package/src/checks/SK018-test-files-run.check.ts +6 -2
- package/src/checks/SK019-tests-run-in-units.check.ts +8 -4
- package/src/checks/SK020-self-spawning-check.check.ts +4 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -4
- package/src/checks/SK024-identity-colors.check.ts +5 -3
- package/src/checks/SK025-release-drift.check.ts +4 -5
- package/src/checks/SK026-asks-autoclose.check.ts +4 -4
- package/src/checks/SK027-agent-wiki-links.check.ts +34 -9
- package/src/checks/SK028-readme.check.ts +10 -2
- package/src/checks/SK029-explicit-check-files.check.ts +95 -0
- package/src/cli/cmd_Asks.ts +5 -5
- package/src/cli/cmd_Assets.ts +7 -7
- package/src/cli/cmd_Bump.ts +12 -11
- package/src/cli/cmd_Check.ts +8 -7
- package/src/cli/cmd_Checks.ts +7 -6
- package/src/cli/cmd_Ci.ts +12 -11
- package/src/cli/cmd_Commands.ts +16 -13
- package/src/cli/cmd_ConfigKeys.ts +7 -6
- package/src/cli/cmd_Contracts.ts +3 -3
- package/src/cli/cmd_CrossRepoActivity.ts +10 -6
- package/src/cli/cmd_Fleet.ts +9 -7
- package/src/cli/cmd_GlobalProcesses.ts +116 -4
- package/src/cli/cmd_Home.ts +23 -17
- package/src/cli/cmd_Hooks.ts +6 -5
- package/src/cli/cmd_Init.ts +5 -4
- package/src/cli/cmd_Keys.ts +8 -7
- package/src/cli/cmd_Mv.ts +7 -6
- package/src/cli/cmd_Needs.ts +4 -3
- package/src/cli/cmd_Overview.ts +4 -3
- package/src/cli/cmd_Pb.ts +2 -2
- package/src/cli/cmd_Planned.ts +6 -5
- package/src/cli/cmd_Prose.ts +5 -5
- package/src/cli/cmd_Reflect.ts +7 -7
- package/src/cli/cmd_Release.ts +6 -5
- package/src/cli/cmd_Releases.ts +5 -4
- package/src/cli/cmd_Serve.ts +4 -3
- package/src/cli/cmd_Smoke.ts +5 -4
- package/src/cli/cmd_Start.ts +7 -6
- package/src/cli/cmd_Status.ts +4 -3
- package/src/cli/cmd_Tui.ts +3 -2
- package/src/cli/cmd_When.ts +71 -0
- package/src/cli/cmd_Wiki.ts +4 -4
- package/src/cli.ts +7 -2
- package/src/collections/checks.ts +2 -0
- package/src/collections/collection-types.ts +24 -4
- package/src/collections/providers.ts +6 -2
- package/src/contracts/apply-contracts.ts +9 -8
- package/src/contracts/contract-types.ts +8 -2
- package/src/contracts/contracts-engine.ts +34 -13
- package/src/contracts/manifests/identity.contract.ts +6 -5
- package/src/contracts/manifests/social.contract.ts +4 -2
- package/src/contracts/manifests/steam.contract.ts +7 -3
- package/src/contracts/web-surface.ts +59 -0
- package/src/core/budget.ts +19 -9
- package/src/core/canonical-scripts.ts +1 -1
- package/src/core/check-runner.ts +63 -25
- package/src/core/config-rewrite.ts +37 -24
- package/src/core/discover-checks.ts +31 -7
- package/src/core/layout.ts +2 -0
- package/src/core/legacy-shapes.ts +39 -10
- package/src/core/motherhome.ts +88 -66
- package/src/core/one-way-in-policy.ts +2 -2
- package/src/core/outcome-words.ts +18 -2
- package/src/core/port-check-cli.ts +52 -0
- package/src/core/port-check-file.ts +295 -0
- package/src/core/port-check-files.ts +31 -0
- package/src/core/retention.ts +0 -14
- package/src/core/run-check.ts +14 -14
- package/src/core/run-ci.ts +11 -7
- package/src/core/run-fix.ts +30 -23
- package/src/core/run-fixers.ts +42 -5
- package/src/core/runs-matrix.ts +179 -0
- package/src/core/runs.ts +242 -0
- package/src/core/update-policy.ts +20 -0
- package/src/core/when-block.ts +29 -30
- package/src/core/when.ts +41 -103
- package/src/core/wire.ts +34 -9
- package/src/fleet/scan-fleet.ts +20 -4
- package/src/fleet/update-fleet.ts +6 -6
- package/src/home/activity-shape.ts +4 -1
- package/src/home/activity.ts +22 -15
- package/src/home/gui-app/ShipkitGui.swift +144 -25
- package/src/home/gui-app.ts +93 -30
- package/src/home/home-launchd.ts +30 -6
- package/src/home/home-routes.ts +14 -2
- package/src/home/home-start.ts +33 -5
- package/src/home/hubs.ts +5 -5
- package/src/home/machine-stores.ts +4 -2
- package/src/home/open-app.ts +23 -10
- package/src/home/pm-config.ts +15 -11
- package/src/home/pm-inputs.ts +76 -0
- package/src/home/pm.ts +11 -3
- package/src/home/process-rows.ts +23 -6
- package/src/home/resources.ts +67 -15
- package/src/home/run-daemon.ts +6 -1
- package/src/home/run-pm.ts +30 -5
- package/src/home/serve-web.ts +2 -2
- package/src/home/supervise.ts +24 -1
- package/src/home/widgets.ts +32 -3
- package/src/hooks/commit-message.ts +2 -3
- package/src/hooks/run-hooks.ts +60 -6
- package/src/init-repo.ts +6 -6
- package/src/ledger/gate-checks.ts +115 -61
- package/src/ledger/git-key.ts +5 -1
- package/src/ledger/ledger.ts +5 -5
- package/src/ledger/store.ts +11 -0
- package/src/ledger/toolchain-identity.ts +45 -27
- package/src/ledger/world-cache.ts +17 -15
- package/src/plan/repo-source.ts +18 -39
- package/src/release/run-release.ts +5 -0
- package/src/repo-config.ts +36 -56
- package/src/rules/rule.ts +2 -2
- package/src/scaffold/files/agent-coding.md +1 -1
- package/src/scaffold/files/agent-index.md +1 -1
- package/src/scaffold/files/verify-skill.md +2 -2
- package/src/serve/hub-app/activity-st.ts +5 -4
- package/src/serve/hub-app/activity-view.tsx +4 -4
- package/src/serve/hub-app/admin-view.tsx +13 -11
- package/src/serve/hub-app/check-view.tsx +66 -46
- package/src/serve/hub-app/check-words.ts +1 -11
- package/src/serve/hub-app/checks-svc.ts +16 -36
- package/src/serve/hub-app/col-tables-view.tsx +131 -0
- package/src/serve/hub-app/col-tables.ts +115 -0
- package/src/serve/hub-app/cols-svc.ts +33 -4
- package/src/serve/hub-app/dock-st.ts +3 -2
- package/src/serve/hub-app/dock-view.tsx +36 -0
- package/src/serve/hub-app/dock.css +7 -1
- package/src/serve/hub-app/float-cards.tsx +30 -0
- package/src/serve/hub-app/floating.tsx +41 -4
- package/src/serve/hub-app/git-st.ts +42 -0
- package/src/serve/hub-app/git-view.tsx +61 -0
- package/src/serve/hub-app/hash-route.ts +45 -1
- package/src/serve/hub-app/header-bar.tsx +62 -72
- package/src/serve/hub-app/health-svc.ts +3 -2
- package/src/serve/hub-app/home-st.ts +15 -3
- package/src/serve/hub-app/home-strip.css +69 -19
- package/src/serve/hub-app/home-strip.tsx +192 -118
- package/src/serve/hub-app/home.html +1 -0
- package/src/serve/hub-app/hub-app.tsx +21 -5
- package/src/serve/hub-app/hub-types.ts +11 -5
- package/src/serve/hub-app/hub.css +88 -44
- package/src/serve/hub-app/icons.tsx +15 -0
- package/src/serve/hub-app/index.html +1 -0
- package/src/serve/hub-app/keys.ts +11 -8
- package/src/serve/hub-app/live-svc.ts +3 -2
- package/src/serve/hub-app/machine-st.ts +34 -49
- package/src/serve/hub-app/palette-st.ts +5 -5
- package/src/serve/hub-app/palette-view.tsx +1 -1
- package/src/serve/hub-app/plan-view.tsx +51 -0
- package/src/serve/hub-app/planned-st.ts +9 -4
- package/src/serve/hub-app/processes-view.tsx +38 -0
- package/src/serve/hub-app/project-list.tsx +3 -3
- package/src/serve/hub-app/projects-svc.ts +15 -10
- package/src/serve/hub-app/prose-hits-st.ts +3 -2
- package/src/serve/hub-app/reach.ts +36 -0
- package/src/serve/hub-app/readme-svc.ts +3 -2
- package/src/serve/hub-app/repo-sections.ts +117 -0
- package/src/serve/hub-app/repo-st.ts +47 -10
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/sidebar.tsx +12 -37
- package/src/serve/hub-app/timeline-svc.ts +3 -2
- package/src/serve/hub-app/timeline-view.tsx +14 -11
- package/src/serve/hub-app/tree-build.ts +149 -155
- package/src/serve/hub-app/tree-row.tsx +39 -9
- package/src/serve/hub-app/tree-svc.ts +70 -49
- package/src/serve/hub-app/when-view.tsx +89 -0
- package/src/serve/hub-app/when.css +17 -0
- package/src/serve/hub-runtime.ts +3 -16
- package/src/serve/project-gutter.ts +1 -2
- package/src/serve/run-serve.ts +1 -1
- package/src/serve/serve-hub.ts +4 -14
- package/src/serve/warm-cell.ts +49 -16
- package/src/suite/judge-suites.ts +42 -17
- package/src/suite/suite-cli.ts +5 -8
- package/src/wiki/lint-md.ts +2 -0
- package/src/wiki/scan-md.ts +37 -10
- package/src/serve/hub-app/processes-svc.ts +0 -63
- package/src/serve/hub-app/repo-sections.tsx +0 -61
- package/src/serve/repo-processes.ts +0 -89
|
@@ -30,7 +30,7 @@ export function conditionalBodies(_opts: ScriptStyle = {}): Record<string, strin
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
// ONE WAY IN (build/shipkit.md): the verification verbs an agent types from
|
|
33
|
+
// ONE WAY IN (build/shipkit/one-way-in.md): the verification verbs an agent types from
|
|
34
34
|
// muscle memory ROUTE to shipkit, so `bun run test` gets the cache and the
|
|
35
35
|
// job attachment without anyone having to remember a rule. The tool body they
|
|
36
36
|
// used to hold moves to `<name>:raw`, which is what shipkit itself runs and
|
package/src/core/check-runner.ts
CHANGED
|
@@ -40,15 +40,19 @@ import { machinePool, SLOT_HELD_ENV, takeSlot, type SlotPool } from './slots.ts'
|
|
|
40
40
|
import { budgetLine, machineLoad } from './budget.ts'
|
|
41
41
|
import { killTree, spawnWithTimeout } from './spawn-timeout.ts'
|
|
42
42
|
import { FOREVER_MS, formatLine, parseWireOutput, type Line } from './wire.ts'
|
|
43
|
-
import { offLine, type
|
|
43
|
+
import { offLine, runsIn, type ResolvedRuns, type RunContext } from './runs.ts'
|
|
44
44
|
import { runCheckBody, type CheckDef, type CheckEnv } from '../check.ts'
|
|
45
45
|
import { Ledger, stampCached } from '../ledger/ledger.ts'
|
|
46
|
-
import { attachedStamp } from './outcome-words.ts'
|
|
46
|
+
import { attachedStamp, ranStamp } from './outcome-words.ts'
|
|
47
47
|
import type { RunLog, RunStatus } from '../ledger/run-log.ts'
|
|
48
48
|
import { liveStateOf } from '../ledger/live-runs.ts'
|
|
49
49
|
import { CHECK_ID_ENV } from '../ledger/suite-log.ts'
|
|
50
50
|
import { rememberWorld, replayWorld, worldKeyOf, type WorldCache } from '../ledger/world-cache.ts'
|
|
51
51
|
import { extractProblems } from '../problems/extract.ts'
|
|
52
|
+
|
|
53
|
+
/** the first line of a check that ran in this run, stamped with its cost. A
|
|
54
|
+
* world answer carries its keep in a stamp already, so the cost joins it */
|
|
55
|
+
const ranHere = (l: Line, ms: number): Line => (l.stamp != null && l.stamp.cached !== true ? { ...l, stamp: { ...l.stamp, ranMs: ms } } : { ...l, text: `${l.text} ${ranStamp(ms)}` })
|
|
52
56
|
import { feedsProblemsPane, type Population, type Problem } from '../problems/problem-line.ts'
|
|
53
57
|
|
|
54
58
|
/** the ceiling a check takes when it names none. A build step's floor
|
|
@@ -56,6 +60,10 @@ import { feedsProblemsPane, type Population, type Problem } from '../problems/pr
|
|
|
56
60
|
* about the world. */
|
|
57
61
|
export const CHECK_TIMEOUT_MS = 120_000
|
|
58
62
|
|
|
63
|
+
/** the most findings a failed build step keeps beside its headline: a tool
|
|
64
|
+
* that prints a wall of them is read in its log */
|
|
65
|
+
const MAX_EXTRA = 20
|
|
66
|
+
|
|
59
67
|
/** epoch ms that never goes backwards inside one process, whole numbers so a
|
|
60
68
|
* row's `at - ms` is its start exactly: the wall clock can step back (an NTP
|
|
61
69
|
* correction), and a check stamped before an earlier one ended makes the
|
|
@@ -78,7 +86,8 @@ export type RunnableCheck = {
|
|
|
78
86
|
blocking: boolean | null
|
|
79
87
|
/** a build step: the gate runs it, the health pass does not */
|
|
80
88
|
build: boolean
|
|
81
|
-
|
|
89
|
+
/** every context it runs in, resolved (core/runs.ts) */
|
|
90
|
+
runs: ResolvedRuns
|
|
82
91
|
deps: string[]
|
|
83
92
|
group: string | null
|
|
84
93
|
timeoutMs: number
|
|
@@ -155,8 +164,10 @@ export type RunChecksOpts = {
|
|
|
155
164
|
root: string
|
|
156
165
|
config: CheckEnv['config']
|
|
157
166
|
audience: Audience
|
|
158
|
-
/**
|
|
167
|
+
/** the full tests tier: `onFull` decides instead of `onCi` */
|
|
159
168
|
fullTier: boolean
|
|
169
|
+
/** the release preflight: `onRelease` decides, whatever the tier */
|
|
170
|
+
release?: boolean
|
|
160
171
|
/** where a `cmd` check's whole output is streamed */
|
|
161
172
|
logDir: string
|
|
162
173
|
/** the world answers' store, null = nothing is remembered by TTL */
|
|
@@ -252,10 +263,13 @@ export type Selection = {
|
|
|
252
263
|
errors: string[]
|
|
253
264
|
}
|
|
254
265
|
|
|
266
|
+
/** the axis each context reads, as a parked line names it */
|
|
267
|
+
const CTX_AXIS: Record<RunContext, string> = { preCommit: 'onPreCommit', ci: 'onCi', full: 'onFull', release: 'onRelease', health: 'onHealth' }
|
|
268
|
+
|
|
255
269
|
/** what this run asks about. The audience decides the shape of the question,
|
|
256
270
|
* `--only` and `--with` name checks past any filter, and OFF is the one word
|
|
257
271
|
* no flag lifts. */
|
|
258
|
-
export function selectChecks(o: Pick<RunChecksOpts, 'checks' | 'audience' | 'fullTier' | 'skipGroups' | 'onlyIds' | 'withIds' | 'onlyBlocking' | 'buildOnly' | 'exclude' | 'includeProactive'>): Selection {
|
|
272
|
+
export function selectChecks(o: Pick<RunChecksOpts, 'checks' | 'audience' | 'fullTier' | 'release' | 'skipGroups' | 'onlyIds' | 'withIds' | 'onlyBlocking' | 'buildOnly' | 'exclude' | 'includeProactive'>): Selection {
|
|
259
273
|
const errors: string[] = []
|
|
260
274
|
const ids = new Set(o.checks.map((u) => u.id))
|
|
261
275
|
if (ids.size !== o.checks.length) errors.push('duplicate check ids')
|
|
@@ -314,26 +328,28 @@ export function selectChecks(o: Pick<RunChecksOpts, 'checks' | 'audience' | 'ful
|
|
|
314
328
|
if (u.blocking == null) undeclared.push(u.id)
|
|
315
329
|
selected.push(u)
|
|
316
330
|
}
|
|
331
|
+
// the one context this run asks about: every row declares whether it
|
|
332
|
+
// runs there (core/runs.ts), and nothing is guessed from its kind
|
|
333
|
+
const ctx: RunContext = o.audience === 'health' ? 'health' : o.release === true ? 'release' : o.fullTier ? 'full' : 'ci'
|
|
317
334
|
for (const u of o.checks) {
|
|
318
|
-
// IS IT THIS RUN'S BUSINESS AT ALL? The
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
// A check out of scope here is not even reported as off: it is not part
|
|
323
|
-
// of this question.
|
|
335
|
+
// IS IT THIS RUN'S BUSINESS AT ALL? The TUI's build panel asks for
|
|
336
|
+
// nothing but build steps, and a sub-unit release skips what its own
|
|
337
|
+
// gate ran. A check out of scope here is not even reported as off: it
|
|
338
|
+
// is not part of this question.
|
|
324
339
|
if (!named.has(u.id)) {
|
|
325
|
-
if (o.audience === 'health' && u.build) continue
|
|
326
340
|
if (o.buildOnly === true && !u.build) continue
|
|
327
341
|
if (excluded.has(u.id)) { parked.push({ id: u.id, text: `${u.id} (not part of this release, its own gate covers it)` }); continue }
|
|
328
342
|
}
|
|
329
343
|
// OFF is the one word that means never, past every other flag: a
|
|
330
344
|
// switch a filter can lift is a suggestion
|
|
331
|
-
if (u.
|
|
345
|
+
if (u.runs.off != null) { off.push(u); continue }
|
|
332
346
|
if (named.has(u.id)) { take(u); continue }
|
|
333
347
|
if (u.group != null && skipGroups.has(u.group)) { parked.push({ id: u.id, text: `${u.id} (--skip ${u.group})` }); continue }
|
|
334
|
-
if (
|
|
335
|
-
|
|
336
|
-
if (
|
|
348
|
+
if (!runsIn(u.runs, ctx)) {
|
|
349
|
+
// the health pass answers its own question and lists nothing else
|
|
350
|
+
if (ctx === 'health') continue
|
|
351
|
+
// release.requireHealthy asks the gate for the health probes too
|
|
352
|
+
if (!(o.includeProactive === true && u.runs.onHealth && !u.build)) { parked.push({ id: u.id, text: `${u.id} (not ${CTX_AXIS[ctx]})` }); continue }
|
|
337
353
|
}
|
|
338
354
|
if (o.onlyBlocking === true && u.blocking !== true) { if (u.blocking == null) undeclared.push(u.id); nonBlocking.set(u.id, u); continue }
|
|
339
355
|
take(u)
|
|
@@ -431,6 +447,9 @@ type Live = RunnableCheck & CheckResult & {
|
|
|
431
447
|
/** what the headline carries after the id: a failure's last line, or the
|
|
432
448
|
* reason it never ran */
|
|
433
449
|
tail: string
|
|
450
|
+
/** a failed build step's other findings, the wire lines its tool printed
|
|
451
|
+
* beside the one the headline carries */
|
|
452
|
+
extra: Line[]
|
|
434
453
|
}
|
|
435
454
|
|
|
436
455
|
export async function runChecks(o: RunChecksOpts): Promise<RunChecksSummary> {
|
|
@@ -454,14 +473,17 @@ export async function runChecks(o: RunChecksOpts): Promise<RunChecksSummary> {
|
|
|
454
473
|
blocking: u.blocking, advisory: r.advisory, lines: r.lines, problems: r.problems, logPath: r.logPath, lastLine: r.lastLine, note: r.note, attached,
|
|
455
474
|
})
|
|
456
475
|
// an attached check is filed as a replay: it measured nothing, and a
|
|
457
|
-
// row carrying its wait would read as a check running in this process
|
|
458
|
-
|
|
476
|
+
// row carrying its wait would read as a check running in this process.
|
|
477
|
+
// A non-blocking failure is filed as the warning the gate printed, so
|
|
478
|
+
// no surface reading the record paints red what the gate called orange
|
|
479
|
+
const filed = r.status === 'fail' && r.advisory ? 'warn' : r.status
|
|
480
|
+
o.log?.add({ id: u.id, status: filed, ms: r.ms, replayed: r.replayed || attached, lines: r.lines, at })
|
|
459
481
|
}
|
|
460
482
|
|
|
461
483
|
// OFF, in ONE place: skipped by name, never run, never stored, and the
|
|
462
484
|
// reason printed where the verdict would have been
|
|
463
485
|
for (const u of [...sel.off, ...(sel.offElsewhere ?? [])]) {
|
|
464
|
-
const line: Line = { kind: 'skip', text: offLine(u.id, u.
|
|
486
|
+
const line: Line = { kind: 'skip', text: offLine(u.id, u.runs) }
|
|
465
487
|
if (sel.off.includes(u)) emit(line); else (o.detail ?? print)(formatLine(line, color))
|
|
466
488
|
report(u, { id: u.id, status: 'skip', advisory: false, replayed: false, ms: 0, exitCode: null, lines: [line], problems: [], lastLine: line.text, note: 'off', logPath: null })
|
|
467
489
|
}
|
|
@@ -476,7 +498,7 @@ export async function runChecks(o: RunChecksOpts): Promise<RunChecksSummary> {
|
|
|
476
498
|
const runDir = openRunDir(o.logDir)
|
|
477
499
|
const checks: Live[] = sel.selected.map((u) => ({
|
|
478
500
|
...u,
|
|
479
|
-
status: 'skip', advisory: false, replayed: false, ms: 0, exitCode: null, lines: [], problems: [], lastLine: '', note: '', startedAt: 0, endedAt: 0, unmeasured: false, attached: false, reads: null, synthetic: false, tail: '',
|
|
501
|
+
status: 'skip', advisory: false, replayed: false, ms: 0, exitCode: null, lines: [], problems: [], lastLine: '', note: '', startedAt: 0, endedAt: 0, unmeasured: false, attached: false, reads: null, synthetic: false, tail: '', extra: [],
|
|
480
502
|
logPath: u.body.cmd != null ? join(runDir, `${u.id.replace(/[^a-zA-Z0-9-]/g, '_')}.log`) : null,
|
|
481
503
|
}))
|
|
482
504
|
const selectedIds = new Set(checks.map((u) => u.id))
|
|
@@ -556,9 +578,14 @@ export async function runChecks(o: RunChecksOpts): Promise<RunChecksSummary> {
|
|
|
556
578
|
const ran = u.status === 'pass' || u.status === 'warn' ? testsRan(u.lastLine) : null
|
|
557
579
|
const counted = ran != null && head != null && head.kind === 'ok' && (head.text === u.id || head.text.startsWith(`${u.id} (`))
|
|
558
580
|
const id = counted ? `${u.id} ${ran} test${ran === 1 ? '' : 's'}` : u.id
|
|
559
|
-
const
|
|
560
|
-
? [{ ...head, text: `${id}
|
|
581
|
+
const composed = u.synthetic && !u.replayed && head != null
|
|
582
|
+
? [{ ...head, text: `${id} ${ranStamp(u.ms)}${u.tail}` }, ...u.lines.slice(1)]
|
|
561
583
|
: counted ? [{ ...head, text: `${id}${head.text.slice(u.id.length)}` }, ...u.lines.slice(1)] : u.lines
|
|
584
|
+
// every line says where its answer came from: a replay carries its
|
|
585
|
+
// cached stamp already, so one that ran here says so and what it cost
|
|
586
|
+
const shown = u.synthetic || u.replayed || u.attached || u.startedAt === 0 || composed[0] == null
|
|
587
|
+
? composed
|
|
588
|
+
: [ranHere(composed[0], u.ms), ...composed.slice(1)]
|
|
562
589
|
// a caller printing its own verdict wants one line per green check and
|
|
563
590
|
// only the warnings of a warned one; a failure prints whole
|
|
564
591
|
const quiet = (l: Line, i: number): boolean =>
|
|
@@ -575,7 +602,7 @@ export async function runChecks(o: RunChecksOpts): Promise<RunChecksSummary> {
|
|
|
575
602
|
const hit = o.ledger?.replay(u.id)
|
|
576
603
|
if (hit != null) {
|
|
577
604
|
u.replayed = true
|
|
578
|
-
u.lines = stampCached(hit.lines, hit.measuredMs)
|
|
605
|
+
u.lines = stampCached(hit.lines, hit.measuredMs, hit.at, clock())
|
|
579
606
|
u.lastLine = hit.lastLine ?? ''
|
|
580
607
|
u.status = hit.status
|
|
581
608
|
// a replay stands for the verdict it replays, findings included:
|
|
@@ -670,14 +697,25 @@ export async function runChecks(o: RunChecksOpts): Promise<RunChecksSummary> {
|
|
|
670
697
|
u.problems = findings(u, u.lines, r.out)
|
|
671
698
|
const lastFail = u.lines.filter((l) => l.kind === 'fail').at(-1)
|
|
672
699
|
if (lastFail != null) u.lastLine = lastFail.text.slice(0, 200)
|
|
700
|
+
// a tool that speaks the wire (the suite runner) ends on its FAIL
|
|
701
|
+
// lines, and the headline carries only the last one. Only that closing
|
|
702
|
+
// block is its report: a wire line earlier in the log is some test's
|
|
703
|
+
// own output
|
|
704
|
+
if (u.build && u.status === 'fail') {
|
|
705
|
+
const parsed = parseWireOutput(r.out)
|
|
706
|
+
let i = parsed.length
|
|
707
|
+
while (i > 0 && (parsed[i - 1]!.kind === 'fail' || parsed[i - 1]!.kind === 'fix-fail')) i--
|
|
708
|
+
u.extra = parsed.slice(i).filter((l) => !(l.kind === 'fail' && `FAIL: ${l.text}` === u.lastLine)).slice(-MAX_EXTRA)
|
|
709
|
+
}
|
|
673
710
|
}
|
|
674
711
|
|
|
675
712
|
/** a check that printed nothing of its own still has to report that it
|
|
676
713
|
* ran: the verdict, its name, and (at print time) what it cost */
|
|
677
714
|
const headlineOf = (u: Live): void => {
|
|
678
715
|
u.synthetic = true
|
|
679
|
-
|
|
680
|
-
u.
|
|
716
|
+
const more = u.extra.filter((l) => l.kind === 'fail').length
|
|
717
|
+
u.tail = u.status === 'fail' ? (u.lastLine === '' ? '' : ` — ${u.lastLine}${more > 0 ? ` (+${more} more below)` : ''}`) : u.status === 'skip' ? (u.note === '' ? '' : ` (${u.note})`) : ''
|
|
718
|
+
u.lines = [{ kind: u.status === 'fail' ? 'fail' : u.status === 'skip' ? 'skip' : 'ok', text: `${u.id}${u.tail}` }, ...u.extra]
|
|
681
719
|
}
|
|
682
720
|
|
|
683
721
|
/** what an attached asker adopts: the verdict and the lines BEFORE this
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
// THE CONFIG HALF of the SK016
|
|
1
|
+
// THE CONFIG HALF of the SK016 and SK029 migrations, in its own process. Writing the
|
|
2
2
|
// managed block and reading the result back both LOAD repo.config.ts as a
|
|
3
3
|
// module, which no check context read can stand for, so the fixer reaches it
|
|
4
4
|
// through `c.exec` and the check file itself never imports the loader.
|
|
5
5
|
//
|
|
6
6
|
// bun config-rewrite.ts <root> '<plan json>' → one json line on stdout
|
|
7
7
|
//
|
|
8
|
-
// The order is the migration's:
|
|
9
|
-
// dropped
|
|
10
|
-
//
|
|
11
|
-
//
|
|
8
|
+
// The order is the migration's: a key the managed block holds (the retired
|
|
9
|
+
// `when`) is dropped first, so the block describes the config again, then the
|
|
10
|
+
// `off` entries are written into the block, then each other key is dropped
|
|
11
|
+
// only once what leaned on it has a home. The result is read back through the
|
|
12
|
+
// loader and the previous bytes go back when it does not survive.
|
|
12
13
|
import { readFileSync } from 'fs'
|
|
13
14
|
import { join } from 'path'
|
|
14
15
|
import { atomicWrite } from './locked-write.ts'
|
|
@@ -17,10 +18,11 @@ import { writeGateBlock } from './when-block.ts'
|
|
|
17
18
|
import { loadRepoConfig } from '../repo-config.ts'
|
|
18
19
|
|
|
19
20
|
export type ConfigPlan = {
|
|
20
|
-
/** check id →
|
|
21
|
-
|
|
22
|
-
/** keys to drop, in order. `
|
|
23
|
-
|
|
21
|
+
/** check id → why it is off, written into the managed block */
|
|
22
|
+
off: Record<string, string>
|
|
23
|
+
/** keys to drop, in order. `first` drops before the block is written (a
|
|
24
|
+
* key inside it), `afterOff` only once the block took the entries */
|
|
25
|
+
drop: { key: string; why: string; phase?: 'first' | 'afterOff' }[]
|
|
24
26
|
/** the config's `name` before the rewrite: what the read back must still say */
|
|
25
27
|
name: string
|
|
26
28
|
}
|
|
@@ -35,27 +37,38 @@ export type ConfigRewrite = {
|
|
|
35
37
|
export async function rewriteConfig(root: string, plan: ConfigPlan): Promise<ConfigRewrite> {
|
|
36
38
|
const did: string[] = []
|
|
37
39
|
const hand: string[] = []
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
const path = join(root, 'repo.config.ts')
|
|
41
|
+
const before = readFileSync(path, 'utf8')
|
|
42
|
+
const drop = (text: string, d: ConfigPlan['drop'][number]): string => {
|
|
43
|
+
const next = dropConfigKey(text, d.key)
|
|
44
|
+
if (next == null) { hand.push(`repo.config.ts \`${d.key}\`: ${d.why}, and the key could not be read mechanically — delete it by hand`); return text }
|
|
45
|
+
did.push(`repo.config.ts drops \`${d.key}\``)
|
|
46
|
+
return next
|
|
47
|
+
}
|
|
48
|
+
let first = before
|
|
49
|
+
for (const d of plan.drop.filter((x) => x.phase === 'first')) first = drop(first, d)
|
|
50
|
+
if (first !== before) atomicWrite(path, first)
|
|
51
|
+
let offWritten = false
|
|
52
|
+
if (Object.keys(plan.off).length > 0) {
|
|
40
53
|
try {
|
|
41
|
-
await writeGateBlock(root, {
|
|
42
|
-
|
|
43
|
-
did.push(`${Object.keys(plan.
|
|
54
|
+
await writeGateBlock(root, { off: plan.off })
|
|
55
|
+
offWritten = true
|
|
56
|
+
did.push(`${Object.keys(plan.off).length} off entr(ies) written into the managed block`)
|
|
44
57
|
} catch (e) {
|
|
45
|
-
|
|
58
|
+
// a key dropped first held these entries: without the block they
|
|
59
|
+
// would be gone, so the file goes back as it was
|
|
60
|
+
if (first !== before) { atomicWrite(path, before); return { did: [], hand, restored: `the managed block refused off ${JSON.stringify(plan.off)}: ${String(e).slice(0, 160)}` } }
|
|
61
|
+
hand.push(`the managed block refused the rewrite (${String(e).slice(0, 160)}) — write off: ${JSON.stringify(plan.off)} into it by hand`)
|
|
46
62
|
}
|
|
47
63
|
}
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const next = dropConfigKey(text, d.key)
|
|
54
|
-
if (next == null) hand.push(`repo.config.ts \`${d.key}\`: ${d.why}, and the key could not be read mechanically — delete it by hand`)
|
|
55
|
-
else { text = next; did.push(`repo.config.ts drops \`${d.key}\``) }
|
|
64
|
+
const mid = readFileSync(path, 'utf8')
|
|
65
|
+
let text = mid
|
|
66
|
+
for (const d of plan.drop.filter((x) => x.phase !== 'first')) {
|
|
67
|
+
if (d.phase === 'afterOff' && !offWritten) continue
|
|
68
|
+
text = drop(text, d)
|
|
56
69
|
}
|
|
70
|
+
if (text !== mid) atomicWrite(path, text)
|
|
57
71
|
if (text !== before) {
|
|
58
|
-
atomicWrite(path, text)
|
|
59
72
|
try {
|
|
60
73
|
const after = await loadRepoConfig(root, { fresh: true })
|
|
61
74
|
if (after.name !== plan.name) throw new Error(`the config reads as '${String(after.name)}'`)
|
|
@@ -56,20 +56,23 @@ export type DiscoveredCheck = {
|
|
|
56
56
|
from: CheckFrom
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
function walk(dir: string, out: string[], suffix = SUFFIX): void {
|
|
59
|
+
function walk(dir: string, out: string[], suffix = SUFFIX, dirs?: string[]): void {
|
|
60
|
+
dirs?.push(dir)
|
|
60
61
|
let entries
|
|
61
62
|
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
62
63
|
for (const e of entries) {
|
|
63
64
|
if (e.isDirectory()) {
|
|
64
|
-
if (!IGNORED_DIRS.has(e.name) && !e.name.startsWith('.')) walk(join(dir, e.name), out, suffix)
|
|
65
|
+
if (!IGNORED_DIRS.has(e.name) && !e.name.startsWith('.')) walk(join(dir, e.name), out, suffix, dirs)
|
|
65
66
|
} else if (e.name.endsWith(suffix)) out.push(join(dir, e.name))
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
/** every `*.shipkit.ts` file under a repo root, by the same walk. Sorted
|
|
70
|
-
|
|
70
|
+
/** every `*.shipkit.ts` file under a repo root, by the same walk. Sorted.
|
|
71
|
+
* `dirs` collects every dir the walk read: a file added to one moves its
|
|
72
|
+
* mtime, which is how a reader tells the list moved without walking again */
|
|
73
|
+
export function repoUnitFiles(root: string, dirs?: string[]): string[] {
|
|
71
74
|
const out: string[] = []
|
|
72
|
-
walk(root, out, UNIT_SUFFIX)
|
|
75
|
+
walk(root, out, UNIT_SUFFIX, dirs)
|
|
73
76
|
return out.sort()
|
|
74
77
|
}
|
|
75
78
|
|
|
@@ -107,7 +110,7 @@ export function isLegacyCheckFile(path: string): boolean {
|
|
|
107
110
|
/** every built-in id, from the file names alone. Sync and import free, so
|
|
108
111
|
* `defineRepo` can read the namespace at config load. Read once per
|
|
109
112
|
* process: the toolkit's own files cannot appear mid run, and `defineRepo`
|
|
110
|
-
* asks for the list once per `
|
|
113
|
+
* asks for the list once per `off` entry. */
|
|
111
114
|
let builtinIds: string[] | null = null
|
|
112
115
|
export function builtinCheckIds(): string[] {
|
|
113
116
|
if (builtinIds != null) return builtinIds
|
|
@@ -141,6 +144,20 @@ async function load(path: string): Promise<CheckDef | null> {
|
|
|
141
144
|
}
|
|
142
145
|
}
|
|
143
146
|
|
|
147
|
+
/** the stand in for a check file that does not load: it fails in every
|
|
148
|
+
* context with the load error, so the file cannot drop out of a gate unseen */
|
|
149
|
+
function unloadable(path: string, message: string): CheckDef {
|
|
150
|
+
return {
|
|
151
|
+
description: 'the check file loads',
|
|
152
|
+
blocking: true,
|
|
153
|
+
runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: false, onClock: false },
|
|
154
|
+
cache: { by: 'never', why: 'the file does not load' },
|
|
155
|
+
fix: 'none',
|
|
156
|
+
applies: 'always',
|
|
157
|
+
run: (c) => c.fail(`${basename(path)} does not load: ${message.replace(`${path}: `, '')}`, 'shipkit check --fix SK029 ports a retired check file with the values it ran on, and shipkit ci does it in its fix pass'),
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
144
161
|
/** every check this repo can run: its own files, plus the toolkit's. Throws
|
|
145
162
|
* on a registration nobody can act on (a duplicate id, a repo check squatting
|
|
146
163
|
* the reserved prefix, a built-in with no owner page), so a broken
|
|
@@ -167,7 +184,14 @@ export async function discoverChecks(root: string): Promise<DiscoveredCheck[]> {
|
|
|
167
184
|
const ids = new Set<string>()
|
|
168
185
|
for (const f of [...builtins, ...own, ...units]) {
|
|
169
186
|
const isBuiltin = builtins.includes(f)
|
|
170
|
-
|
|
187
|
+
let def: CheckDef | null
|
|
188
|
+
try {
|
|
189
|
+
def = await load(f)
|
|
190
|
+
} catch (e) {
|
|
191
|
+
// a file that does not load is ONE red row, never a dead inventory:
|
|
192
|
+
// every other check still runs, and this one says why in every run
|
|
193
|
+
def = unloadable(f, (e as Error).message)
|
|
194
|
+
}
|
|
171
195
|
if (def == null) continue
|
|
172
196
|
const rel = isBuiltin ? null : relative(root, f).replace(/\\/g, '/')
|
|
173
197
|
const fallback = isBuiltin
|
package/src/core/layout.ts
CHANGED
|
@@ -32,6 +32,8 @@ export const LAYOUT = {
|
|
|
32
32
|
reviews: sk('reviews'),
|
|
33
33
|
// scratch: plans, logs, leases, detached runs, screenshots
|
|
34
34
|
tmp: sk('tmp'),
|
|
35
|
+
// built app bundles, one per global process opened as an app (home/gui-app.ts)
|
|
36
|
+
wrapper: sk('wrapper'),
|
|
35
37
|
// generated on every run
|
|
36
38
|
status: sk('STATUS.md'),
|
|
37
39
|
statusFull: sk('STATUS.full.md'),
|
|
@@ -22,6 +22,7 @@ import { fallbackNames, isCheckFilePath, readLegacyCheck, type LegacyCheck } fro
|
|
|
22
22
|
import type { ScanCtx } from './check-ctx.ts'
|
|
23
23
|
import type { RepoConfig } from '../repo-config.ts'
|
|
24
24
|
import { descriptionProblem, type TaskDef } from '../check.ts'
|
|
25
|
+
import { deriveExplicit, type Derived } from './port-check-file.ts'
|
|
25
26
|
|
|
26
27
|
/** the narrow module every rewritten file imports `check` from, and the ONE
|
|
27
28
|
* spelling: the barrel costs 400ms of module load and discovery imports
|
|
@@ -130,16 +131,34 @@ export const PLACEHOLDER_MARK = 'TODO: state what must be true'
|
|
|
130
131
|
* drops none of them. */
|
|
131
132
|
export function scriptPort(read: LegacyCheck, fallbackBlocking: boolean | undefined): string {
|
|
132
133
|
const blocking = read.blocking ?? fallbackBlocking
|
|
134
|
+
// what the ported `run` body resolves to: the script's own declarations,
|
|
135
|
+
// read as the gate read them, on a body keyed on what it reads
|
|
136
|
+
const d = explicitOf(deriveExplicit({
|
|
137
|
+
when: read.gate ? null : 'health',
|
|
138
|
+
build: false,
|
|
139
|
+
body: 'run',
|
|
140
|
+
...(read.inputs != null ? { inputs: `[${read.inputs.map(q).join(', ')}]` } : {}),
|
|
141
|
+
...(read.ttl != null ? { ttl: q(read.ttl) } : {}),
|
|
142
|
+
...(read.never != null ? { never: q(read.never) } : {}),
|
|
143
|
+
repair: false,
|
|
144
|
+
rawAccess: null,
|
|
145
|
+
}, q))
|
|
133
146
|
const fields = [
|
|
134
147
|
blocking == null ? 'blocking: <true | false, it declares none>' : `blocking: ${blocking}`,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
148
|
+
`runs: ${d.runs}`,
|
|
149
|
+
`cache: ${d.cache}`,
|
|
150
|
+
`fix: ${q(d.fix)}`,
|
|
151
|
+
`applies: ${d.applies}`,
|
|
139
152
|
]
|
|
140
153
|
return `a check script, run as its own process: port it by hand into \`export default check({ description, ${fields.join(', ')}, run: (c) => … })\`, each print a call on c (c.ok, c.warn, c.fail)`
|
|
141
154
|
}
|
|
142
155
|
|
|
156
|
+
/** a derivation that cannot fail on what a script or a task can declare */
|
|
157
|
+
function explicitOf(d: Derived | { error: string }): Derived {
|
|
158
|
+
if ('error' in d) throw new Error(`the legacy declaration has no explicit form: ${d.error}`)
|
|
159
|
+
return d
|
|
160
|
+
}
|
|
161
|
+
|
|
143
162
|
/** the check file a task's argv runs through bun, `bun <file>` or `bun run
|
|
144
163
|
* <file>`, relative, `./` prefixed or absolute, resolved against the task's
|
|
145
164
|
* cwd. `rest` is what the argv passes after the file: a task that passes its
|
|
@@ -159,16 +178,24 @@ export function checkFileRunBy(t: Pick<TaskDef, 'cmd' | 'cwd'>, root: string): {
|
|
|
159
178
|
* always was: the gate runs it and the health pass leaves it out. */
|
|
160
179
|
export function taskCheckSource(t: TaskDef, root: string): string {
|
|
161
180
|
const own = t.description != null && descriptionProblem(t.description) == null ? t.description.trim() : null
|
|
162
|
-
|
|
181
|
+
// where it ran and how it was kept, as the gate and the planner read a
|
|
182
|
+
// task: an optIn one ran only by name
|
|
183
|
+
const d = explicitOf(deriveExplicit({
|
|
184
|
+
when: t.optIn === true ? 'health' : null,
|
|
185
|
+
build: true,
|
|
186
|
+
body: 'cmd',
|
|
187
|
+
...(t.inputs != null ? { inputs: `[${t.inputs.map(q).join(', ')}]` } : {}),
|
|
188
|
+
...(t.ttl != null ? { ttl: q(t.ttl) } : {}),
|
|
189
|
+
...(t.never != null ? { never: q(t.never) } : {}),
|
|
190
|
+
repair: false,
|
|
191
|
+
rawAccess: null,
|
|
192
|
+
}, q))
|
|
193
|
+
const fields = [`id: ${q(t.id)},`, `description: ${q(own ?? descriptionPlaceholder())},`, 'blocking: true,', 'build: true,', `runs: ${d.runs},`, `cache: ${d.cache},`, `fix: ${q(d.fix)},`, `applies: ${d.applies},`, `cmd: [${t.cmd!.map(q).join(', ')}],`]
|
|
163
194
|
if (t.deps != null && t.deps.length > 0) fields.push(`deps: [${t.deps.map(q).join(', ')}],`)
|
|
164
|
-
if (t.inputs != null) fields.push(`inputs: [${t.inputs.map(q).join(', ')}],`)
|
|
165
|
-
if (t.ttl != null) fields.push(`ttl: ${q(t.ttl)},`)
|
|
166
|
-
if (t.never != null) fields.push(`never: ${q(t.never)},`)
|
|
167
195
|
if (t.group != null) fields.push(`group: ${q(t.group)},`)
|
|
168
196
|
if (t.timeoutMs != null) fields.push(`timeoutMs: ${t.timeoutMs},`)
|
|
169
197
|
if (t.env != null) fields.push(`env: { ${Object.entries(t.env).map(([k, v]) => `${q(k)}: ${q(v)}`).join(', ')} },`)
|
|
170
198
|
if (t.cwd != null) fields.push(`cwd: ${q(t.cwd)},`)
|
|
171
|
-
if (t.optIn === true) fields.push(`when: 'health',`)
|
|
172
199
|
const w = checkWrapper(root)
|
|
173
200
|
return [...w.head, w.open, ...fields.map((f) => ` ${f}`), w.close, ''].join('\n')
|
|
174
201
|
}
|
|
@@ -189,6 +216,8 @@ export function dropConfigKey(text: string, key: string): string | null {
|
|
|
189
216
|
const start = at[0]!
|
|
190
217
|
const end = statementEnd(lines, start)
|
|
191
218
|
let from = start
|
|
192
|
-
|
|
219
|
+
// the comment block above the key goes with it, never a managed block
|
|
220
|
+
// marker (`// >>> shipkit …`): the key may be the first line inside one
|
|
221
|
+
while (from > 0 && lines[from - 1]!.trim().startsWith('//') && !/^\/\/ (>>>|<<<) shipkit/.test(lines[from - 1]!.trim())) from--
|
|
193
222
|
return [...lines.slice(0, from), ...lines.slice(end + 1)].join('\n')
|
|
194
223
|
}
|