rvlib-shipkit 1.4.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 +68 -0
- package/README.md +17 -17
- package/package.json +3 -2
- package/src/check.ts +68 -95
- package/src/checks/SK001-package-json-shape.check.ts +6 -2
- 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 +4 -2
- 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 +4 -0
- package/src/checks/SK015-nested-repo.check.ts +4 -2
- package/src/checks/SK016-legacy-check-shapes.check.ts +13 -12
- 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 +4 -1
- package/src/checks/SK028-readme.check.ts +5 -1
- 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 +6 -5
- 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 +9 -4
- package/src/core/canonical-scripts.ts +1 -1
- package/src/core/check-runner.ts +37 -20
- package/src/core/config-rewrite.ts +37 -24
- package/src/core/discover-checks.ts +23 -2
- package/src/core/legacy-shapes.ts +39 -10
- 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 +12 -9
- package/src/core/run-ci.ts +8 -4
- 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 +10 -14
- package/src/fleet/scan-fleet.ts +20 -4
- package/src/fleet/update-fleet.ts +6 -6
- package/src/home/home-launchd.ts +20 -4
- package/src/home/home-routes.ts +12 -1
- package/src/home/hubs.ts +5 -5
- package/src/home/pm.ts +8 -0
- package/src/home/run-pm.ts +1 -0
- package/src/home/serve-web.ts +2 -2
- package/src/home/supervise.ts +24 -1
- package/src/hooks/run-hooks.ts +48 -4
- package/src/init-repo.ts +6 -6
- package/src/ledger/gate-checks.ts +113 -59
- package/src/ledger/git-key.ts +5 -1
- package/src/ledger/ledger.ts +5 -5
- package/src/ledger/toolchain-identity.ts +45 -27
- package/src/plan/repo-source.ts +18 -39
- package/src/release/run-release.ts +5 -0
- package/src/repo-config.ts +30 -55
- 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 +1 -1
- 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 +14 -35
- package/src/serve/hub-app/col-tables-view.tsx +13 -6
- package/src/serve/hub-app/cols-svc.ts +5 -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 +6 -0
- 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 +14 -2
- package/src/serve/hub-app/home-strip.css +43 -8
- package/src/serve/hub-app/home-strip.tsx +111 -47
- package/src/serve/hub-app/home.html +1 -0
- package/src/serve/hub-app/hub-app.tsx +14 -4
- package/src/serve/hub-app/hub-types.ts +5 -2
- package/src/serve/hub-app/hub.css +65 -43
- package/src/serve/hub-app/icons.tsx +11 -0
- package/src/serve/hub-app/index.html +1 -0
- package/src/serve/hub-app/keys.ts +5 -2
- package/src/serve/hub-app/live-svc.ts +3 -2
- package/src/serve/hub-app/machine-st.ts +18 -22
- 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/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 +46 -9
- package/src/serve/hub-app/sidebar.tsx +12 -37
- package/src/serve/hub-app/timeline-svc.ts +3 -2
- 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/run-serve.ts +1 -1
- package/src/serve/serve-hub.ts +4 -14
- 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
package/src/home/pm.ts
CHANGED
|
@@ -27,6 +27,10 @@ export type ChildView = ProcView & { tail: string[] }
|
|
|
27
27
|
/** where pm, the daemon and the web listen */
|
|
28
28
|
export type CorePorts = { pm: number; daemon: number; web: number }
|
|
29
29
|
|
|
30
|
+
/** a child whose pid lives but has not answered for this long is hung and
|
|
31
|
+
* gets killed: a healthy daemon answers within about 20 s of its start */
|
|
32
|
+
export const CHILD_HANG_MS = 90_000
|
|
33
|
+
|
|
30
34
|
/** how many log lines each of shipkit's own processes hands the page */
|
|
31
35
|
const CORE_TAIL = 20
|
|
32
36
|
|
|
@@ -61,6 +65,8 @@ export type PmDeps = {
|
|
|
61
65
|
/** runs a stop or restart of the web once its answer is out: the web
|
|
62
66
|
* proxies that very request (default: 200 ms later) */
|
|
63
67
|
later?: (f: () => void) => void
|
|
68
|
+
/** where pm says what it did on its own, a hung child killed */
|
|
69
|
+
say?: (line: string) => void
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
const WEB_ANSWER_MS = 200
|
|
@@ -79,6 +85,8 @@ export class Pm {
|
|
|
79
85
|
probe: () => d.probe(role),
|
|
80
86
|
// a crash loop while its code is being edited heals itself
|
|
81
87
|
neverGiveUp: true,
|
|
88
|
+
hangMs: CHILD_HANG_MS,
|
|
89
|
+
say: d.say,
|
|
82
90
|
}, d.deps ?? realDeps)]))
|
|
83
91
|
}
|
|
84
92
|
|
package/src/home/run-pm.ts
CHANGED
|
@@ -100,6 +100,7 @@ export async function runPm(root: string, io: HomeIo, o: { port?: number } = {})
|
|
|
100
100
|
cmd: (role) => [process.execPath, CLI, role],
|
|
101
101
|
probe: roleAnswers,
|
|
102
102
|
resources: new Resources(c.processes, dir),
|
|
103
|
+
say: io.printErr,
|
|
103
104
|
})
|
|
104
105
|
let server: Bun.Server<unknown>
|
|
105
106
|
try {
|
package/src/home/serve-web.ts
CHANGED
|
@@ -51,8 +51,8 @@ export const REPO_ANSWER_MS = 30_000
|
|
|
51
51
|
export const LIST_EVERY_MS = 30_000
|
|
52
52
|
export const LIST_RETRY_MS = 1_000
|
|
53
53
|
|
|
54
|
-
/** the
|
|
55
|
-
export const readsGit = (path: string): boolean => path === '/api/home/activity.json'
|
|
54
|
+
/** the home answers that can wait on git: the activity page's first read of a window, and a repo's git page */
|
|
55
|
+
export const readsGit = (path: string): boolean => path === '/api/home/activity.json' || path === '/api/home/git.json'
|
|
56
56
|
|
|
57
57
|
/** a run or a check run streams for minutes, and a silent one is never cut */
|
|
58
58
|
export const streams = (path: string): boolean => /\/api\/(run|check\/run)$/.test(path)
|
package/src/home/supervise.ts
CHANGED
|
@@ -29,6 +29,12 @@ export type ProcSpec = {
|
|
|
29
29
|
/** never left down after CRASH_LIMIT crashes: it keeps retrying at the
|
|
30
30
|
* longest backoff, so a code fix brings it back with no hand */
|
|
31
31
|
neverGiveUp?: boolean
|
|
32
|
+
/** a pid that lives but has not answered its probe for this long is hung
|
|
33
|
+
* (a main thread stuck in one synchronous step still holds its port):
|
|
34
|
+
* killed, then started again after the crash backoff. None = never */
|
|
35
|
+
hangMs?: number
|
|
36
|
+
/** a line for every act the supervisor takes on its own */
|
|
37
|
+
say?: (line: string) => void
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
export type ProcView = {
|
|
@@ -137,6 +143,8 @@ export class Proc {
|
|
|
137
143
|
private retryAt: number | null = null
|
|
138
144
|
private killAt: number | null = null
|
|
139
145
|
private probing = false
|
|
146
|
+
/** why the kill in flight was a hang kill, for the crash note */
|
|
147
|
+
private hung: string | null = null
|
|
140
148
|
/** a restart: start again once the stop landed */
|
|
141
149
|
private again = false
|
|
142
150
|
/** it answers its probe with no pid of ours behind it */
|
|
@@ -275,6 +283,8 @@ export class Proc {
|
|
|
275
283
|
const now = this.d.now()
|
|
276
284
|
if (this.pid != null && !this.d.alive(this.pid)) {
|
|
277
285
|
this.gone()
|
|
286
|
+
const hung = this.hung
|
|
287
|
+
this.hung = null
|
|
278
288
|
if (this.again) {
|
|
279
289
|
this.again = false
|
|
280
290
|
this.want = 'up'
|
|
@@ -287,7 +297,7 @@ export class Proc {
|
|
|
287
297
|
this.set('crashed', `exited ${this.crashes.length} times in ${Math.round(CRASH_WINDOW_MS / 60_000)} min, left down: see the log`)
|
|
288
298
|
} else {
|
|
289
299
|
this.retryAt = now + BACKOFF_MS[Math.min(this.crashes.length - 1, BACKOFF_MS.length - 1)]!
|
|
290
|
-
this.set('crashed', `exited, restart in ${Math.round((this.retryAt - now) / 1000)}s`)
|
|
300
|
+
this.set('crashed', `${hung != null ? `${hung}, killed` : 'exited'}, restart in ${Math.round((this.retryAt - now) / 1000)}s`)
|
|
291
301
|
}
|
|
292
302
|
}
|
|
293
303
|
}
|
|
@@ -318,6 +328,19 @@ export class Proc {
|
|
|
318
328
|
} finally {
|
|
319
329
|
this.probing = false
|
|
320
330
|
}
|
|
331
|
+
this.killIfHung(now)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** a pid that lives and still has not answered, hangMs after it last
|
|
335
|
+
* changed status, is stopped: the crash path starts it again */
|
|
336
|
+
private killIfHung(now: number): void {
|
|
337
|
+
const hang = this.spec.hangMs
|
|
338
|
+
if (hang == null || this.status !== 'starting' || this.pid == null || this.want !== 'up' || now - this.since < hang) return
|
|
339
|
+
this.hung = `did not answer its probe for ${Math.round((now - this.since) / 1000)}s`
|
|
340
|
+
this.spec.say?.(`WARN ${this.spec.id} (pid ${this.pid}) ${this.hung}: killed, it starts again`)
|
|
341
|
+
this.d.kill(this.pid, 'SIGTERM')
|
|
342
|
+
this.killAt = now + KILL_AFTER_MS
|
|
343
|
+
this.set('stopping', `${this.hung}, killing it`)
|
|
321
344
|
}
|
|
322
345
|
|
|
323
346
|
view(): ProcView {
|
package/src/hooks/run-hooks.ts
CHANGED
|
@@ -14,6 +14,11 @@ import { gitRoot } from '../core/git-root.ts'
|
|
|
14
14
|
import { spawnWithTimeout } from '../core/spawn-timeout.ts'
|
|
15
15
|
import type { RepoConfig } from '../repo-config.ts'
|
|
16
16
|
import { gitBin } from '../core/git-bin.ts'
|
|
17
|
+
import { discoverChecks } from '../core/discover-checks.ts'
|
|
18
|
+
import { CheckCtx } from '../core/check-ctx.ts'
|
|
19
|
+
import { runCheckBody, type CheckDef } from '../check.ts'
|
|
20
|
+
import { repoOff } from '../core/when.ts'
|
|
21
|
+
import { formatLine } from '../core/wire.ts'
|
|
17
22
|
|
|
18
23
|
export type HooksConfig = {
|
|
19
24
|
/** repo-specific commands run in the pre-commit gate, after the built-ins.
|
|
@@ -210,7 +215,7 @@ export function blockedStaged(staged: string[], config: RepoConfig): string[] {
|
|
|
210
215
|
const draftsOpen = (c: RepoConfig): boolean => c.visibility === 'private'
|
|
211
216
|
|
|
212
217
|
/** the pre-commit gate: built-in leak guard, then the repo's own commands.
|
|
213
|
-
* A commit takes ~1s (build/shipkit.md): the gate times itself, and going
|
|
218
|
+
* A commit takes ~1s (build/shipkit/commit-gates.md): the gate times itself, and going
|
|
214
219
|
* over budget WARNs naming the fix, so drift back to slow commits is loud. */
|
|
215
220
|
export async function runPreCommit(root: string, config: RepoConfig): Promise<GateOutcome[]> {
|
|
216
221
|
const started = performance.now()
|
|
@@ -273,6 +278,7 @@ export async function runPreCommit(root: string, config: RepoConfig): Promise<Ga
|
|
|
273
278
|
],
|
|
274
279
|
})
|
|
275
280
|
}
|
|
281
|
+
out.push(...(await runPreCommitChecks(root, config)))
|
|
276
282
|
for (const c of h.preCommit ?? []) {
|
|
277
283
|
const r = await spawnWithTimeout(c.cmd, { cwd: root, timeoutMs: 120_000 })
|
|
278
284
|
out.push({
|
|
@@ -282,15 +288,52 @@ export async function runPreCommit(root: string, config: RepoConfig): Promise<Ga
|
|
|
282
288
|
})
|
|
283
289
|
}
|
|
284
290
|
const elapsed = performance.now() - started
|
|
285
|
-
if (elapsed >
|
|
291
|
+
if (elapsed > HOOK_BUDGET_MS)
|
|
286
292
|
out.push({
|
|
287
293
|
id: 'hook-budget',
|
|
288
294
|
ok: true,
|
|
289
|
-
lines: [`WARN pre-commit took ${(elapsed / 1000).toFixed(1)}s (budget
|
|
295
|
+
lines: [`WARN pre-commit took ${(elapsed / 1000).toFixed(1)}s (budget ${HOOK_BUDGET_MS / 1000}s) — move slow preCommit commands to ci tasks, or a check off onPreCommit (build/shipkit/runs.md)`],
|
|
290
296
|
})
|
|
291
297
|
return out
|
|
292
298
|
}
|
|
293
299
|
|
|
300
|
+
/** the whole hook, boot included, past which it warns */
|
|
301
|
+
export const HOOK_BUDGET_MS = 1_000
|
|
302
|
+
|
|
303
|
+
/** the checks that declare `onPreCommit` and apply here, with their budget */
|
|
304
|
+
export type PreCommitCheck = { id: string; title: string; owner: string; budgetMs: number; run: NonNullable<CheckDef['run']>; repair: unknown }
|
|
305
|
+
|
|
306
|
+
export async function preCommitChecks(root: string, config: RepoConfig): Promise<PreCommitCheck[]> {
|
|
307
|
+
const out: PreCommitCheck[] = []
|
|
308
|
+
for (const u of await discoverChecks(root)) {
|
|
309
|
+
const p = u.def.runs.onPreCommit
|
|
310
|
+
if (p === false || u.def.run == null) continue
|
|
311
|
+
if (repoOff(config, u.id) != null) continue
|
|
312
|
+
if (typeof u.def.applies === 'function') {
|
|
313
|
+
try { if (u.def.applies({ root, config }) != null) continue } catch (e) { console.error(`WARN ${u.id} applies threw, so it applies: ${(e as Error).message}`) }
|
|
314
|
+
}
|
|
315
|
+
out.push({ id: u.id, title: u.title, owner: u.owner, budgetMs: p.budgetMs, run: u.def.run, repair: u.def.repair })
|
|
316
|
+
}
|
|
317
|
+
return out
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** every `onPreCommit` check, run RAW on the working tree: no ledger, no
|
|
321
|
+
* cache, no repair. A failure refuses the commit, a pass over its budget
|
|
322
|
+
* warns by name and never blocks on time */
|
|
323
|
+
async function runPreCommitChecks(root: string, config: RepoConfig): Promise<GateOutcome[]> {
|
|
324
|
+
const out: GateOutcome[] = []
|
|
325
|
+
for (const k of await preCommitChecks(root, config)) {
|
|
326
|
+
const c = CheckCtx.open({ root, config })
|
|
327
|
+
const t = performance.now()
|
|
328
|
+
await runCheckBody({ id: k.id, title: k.title, owner: k.owner, run: k.run, fix: k.repair }, c)
|
|
329
|
+
const ms = performance.now() - t
|
|
330
|
+
const lines = c.failed ? c.lines.map((l) => formatLine(l, false)) : [`ok: ${k.id} (${Math.round(ms)}ms)`]
|
|
331
|
+
if (ms > k.budgetMs) lines.push(`WARN ${k.id} took ${Math.round(ms)}ms at commit, over its ${k.budgetMs}ms onPreCommit budget — make it cheaper, or move it off onPreCommit`)
|
|
332
|
+
out.push({ id: k.id, ok: !c.failed, lines })
|
|
333
|
+
}
|
|
334
|
+
return out
|
|
335
|
+
}
|
|
336
|
+
|
|
294
337
|
/** the commit-msg gate: the shared message predicate, then the repo's own. */
|
|
295
338
|
export async function runCommitMsg(root: string, config: RepoConfig, messageFile: string): Promise<GateOutcome[]> {
|
|
296
339
|
const h = config.hooks ?? {}
|
|
@@ -339,7 +382,7 @@ export async function runCommitMsg(root: string, config: RepoConfig, messageFile
|
|
|
339
382
|
}
|
|
340
383
|
|
|
341
384
|
/** what blocks a commit here, the enumeration hand-rolled hooks never had. */
|
|
342
|
-
export function listGates(config: RepoConfig, root = process.cwd()): string[] {
|
|
385
|
+
export function listGates(config: RepoConfig, root = process.cwd(), checks: { id: string; budgetMs: number }[] = []): string[] {
|
|
343
386
|
const h = config.hooks ?? {}
|
|
344
387
|
const off = new Set(h.disable ?? [])
|
|
345
388
|
const kw = h.keywordFile ?? KEYWORD_FILE
|
|
@@ -354,6 +397,7 @@ export function listGates(config: RepoConfig, root = process.cwd()): string[] {
|
|
|
354
397
|
`pre-commit leak-guard${leakState} — staged added lines + paths vs ${kw} + ~/.shipkit/private/banned-keywords.txt${leakAllowState}`,
|
|
355
398
|
...((h.blockedPaths ?? []).length > 0 ? [`pre-commit blocked-paths — nothing under ${h.blockedPaths!.join(', ')} enters git${draftsOpen(config) && blockedStaged([COMFY_DRAFTS], config).length === 0 && h.blockedPaths!.some((p) => COMFY_DRAFTS.startsWith(p)) ? `, except ${COMFY_DRAFTS} (declared private)` : ''}`] : []),
|
|
356
399
|
`pre-commit root-identity${off.has('root-identity') ? ' (DISABLED by config)' : ''} — the repo's own name in package.json + repo.config.ts, and scratch files`,
|
|
400
|
+
...checks.map((c) => `pre-commit ${c.id} — the check, onPreCommit, budget ${c.budgetMs}ms`),
|
|
357
401
|
...(h.preCommit ?? []).map((c) => `pre-commit ${c.id} — ${c.cmd.join(' ')}`),
|
|
358
402
|
off.has('commit-message')
|
|
359
403
|
? `commit-msg commit-message (DISABLED by config) — dash punctuation + French; ai-attribution stays on, never disabled`
|
package/src/init-repo.ts
CHANGED
|
@@ -100,7 +100,7 @@ export async function initRepo(root: string, opts: InitOpts = {}): Promise<numbe
|
|
|
100
100
|
// its .gitignore policy block (same fixer as `check --fix SK004`),
|
|
101
101
|
// with the repo's own contributed lines honored
|
|
102
102
|
const cfg = await loadRepoConfig(root)
|
|
103
|
-
const g = await gitignorePolicy.
|
|
103
|
+
const g = await gitignorePolicy.repair!(CheckCtx.open({ root, config: cfg }))
|
|
104
104
|
p(`ok: .gitignore policy block ${g.changed ? 'written' : 'already current'}`)
|
|
105
105
|
// the repos that need ONE WAY IN most are the ones that already exist,
|
|
106
106
|
// so a re-run REPAIRS rather than only seeding a fresh checkout
|
|
@@ -136,7 +136,7 @@ export async function initRepo(root: string, opts: InitOpts = {}): Promise<numbe
|
|
|
136
136
|
}
|
|
137
137
|
// the .gitignore policy block (contract git policy) — same fixer as
|
|
138
138
|
// `shipkit check --fix SK004`, one code path
|
|
139
|
-
const g = await gitignorePolicy.
|
|
139
|
+
const g = await gitignorePolicy.repair!(CheckCtx.open({ root, config: defineRepo({ name }) }))
|
|
140
140
|
p(`ok: .gitignore policy block ${g.changed ? 'written' : 'already current'}`)
|
|
141
141
|
await repairOneWayIn(root, defineRepo({ name }), p)
|
|
142
142
|
await furnishEditor(root, { name }, p)
|
|
@@ -194,7 +194,7 @@ async function furnishEditor(root: string, cfg: { name?: string; serve?: { port?
|
|
|
194
194
|
// a file SK017 cannot read is still the repo's own: it is never
|
|
195
195
|
// reported as missing, and it is never rewritten
|
|
196
196
|
let hub: { changed: boolean; summary: string }
|
|
197
|
-
try { hub = await vscodeHubTask.
|
|
197
|
+
try { hub = await vscodeHubTask.repair!(CheckCtx.open({ root, config: defineRepo({ name }) })) } catch (e) {
|
|
198
198
|
p('ok: .vscode/ is this repo\'s own, left untouched')
|
|
199
199
|
return void p(`WARN .vscode/tasks.json does not parse (${e instanceof Error ? e.message : String(e)}): the hub task gets no problem matcher until it does`)
|
|
200
200
|
}
|
|
@@ -208,14 +208,14 @@ async function furnishEditor(root: string, cfg: { name?: string; serve?: { port?
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
/** ONE WAY IN, both halves, through the SAME fixers `shipkit check --fix` runs
|
|
211
|
-
* (build/shipkit.md). Nothing here is a second implementation: rule SK001
|
|
211
|
+
* (build/shipkit/one-way-in.md). Nothing here is a second implementation: rule SK001
|
|
212
212
|
* routes the package.json scripts, rule SK013 writes the harness policy, and
|
|
213
213
|
* init just calls them so a wired repo is born enforced and a re-run repairs
|
|
214
214
|
* one that was not. The deny is a RESTRICTION, which is why it lands
|
|
215
215
|
* unconditionally where the scaffold's allow list stays opt in. */
|
|
216
216
|
async function repairOneWayIn(root: string, config: Parameters<typeof defineRepo>[0], p: (line: string) => void): Promise<void> {
|
|
217
|
-
const pkg = await packageJsonShape.
|
|
217
|
+
const pkg = await packageJsonShape.repair!(CheckCtx.open({ root, config }))
|
|
218
218
|
if (pkg.changed) p(`ok: package.json — ${pkg.summary}`)
|
|
219
|
-
const policy = await oneWayInPolicy.
|
|
219
|
+
const policy = await oneWayInPolicy.repair!(CheckCtx.open({ root, config }))
|
|
220
220
|
p(policy.changed ? `ok: ${policy.summary}` : `ok: .claude/settings.json already closes every raw runner`)
|
|
221
221
|
}
|
|
@@ -19,11 +19,12 @@ import { discoverLegacyChecks } from '../core/discover-legacy-checks.ts'
|
|
|
19
19
|
import { checkFileRunBy } from '../core/legacy-shapes.ts'
|
|
20
20
|
import { ruleCacheExtra } from '../rules/rule.ts'
|
|
21
21
|
import { discoverChecks } from '../core/discover-checks.ts'
|
|
22
|
-
import { checkCacheExtra, describeFirst, type TaskDef, type CheckFrom, type CheckEnv } from '../check.ts'
|
|
22
|
+
import { checkCacheExtra, describeFirst, type TaskDef, type CheckFrom, type CheckEnv, type CheckDef } from '../check.ts'
|
|
23
23
|
import { CHECK_TIMEOUT_MS, type RunnableCheck, type CheckBody } from '../core/check-runner.ts'
|
|
24
|
-
import { ttlMsOf } from '../core/wire.ts'
|
|
24
|
+
import { FOREVER, ttlMsOf } from '../core/wire.ts'
|
|
25
25
|
import { budgetOf } from '../core/budget.ts'
|
|
26
|
-
import {
|
|
26
|
+
import { repoOff, withRepoOff, whenSummary, type WhenDecl } from '../core/when.ts'
|
|
27
|
+
import { cacheText, filesText, NOWHERE, sourced, type CacheDecl, type ResolvedRuns, type Runs } from '../core/runs.ts'
|
|
27
28
|
import type { CheckDecl } from './git-key.ts'
|
|
28
29
|
import { populationOf } from '../problems/problem-line.ts'
|
|
29
30
|
import { rawAccessInClosure, rawAccessLine } from '../core/raw-access.ts'
|
|
@@ -31,7 +32,6 @@ import type { RvtConfig } from '../ts/config.ts'
|
|
|
31
32
|
import { LIB_ROOT } from '../core/lib-root.ts'
|
|
32
33
|
import type { DiscoveredCheck } from '../core/discover-checks.ts'
|
|
33
34
|
import { declaredTestUnits, inputsOutsideUnits, isTestFile } from '../suite/test-units.ts'
|
|
34
|
-
import type { PlanDecl } from '../plan/cadence.ts'
|
|
35
35
|
|
|
36
36
|
/** why an undeclared `run` check cannot be keyed on its reads, or null when it
|
|
37
37
|
* can. A built-in is walked inside the toolkit, a repo check inside the repo
|
|
@@ -74,18 +74,6 @@ export async function allTasks(c: RepoConfig, root: string): Promise<FloorResolu
|
|
|
74
74
|
/** one gate check with everything a reader asks about it, not only what the
|
|
75
75
|
* key layer asks. The cache half stays in `decl` so `CheckDecl` never grows
|
|
76
76
|
* fields the key layer must then decide to ignore. */
|
|
77
|
-
/** a check's `planOff`, read now; one that throws stays planned, and says so */
|
|
78
|
-
function planOffOf(id: string, fn: ((env: CheckEnv) => string | null) | undefined, env: CheckEnv): { planOff?: string } {
|
|
79
|
-
if (fn == null) return {}
|
|
80
|
-
try {
|
|
81
|
-
const why = fn(env)
|
|
82
|
-
return why == null ? {} : { planOff: why }
|
|
83
|
-
} catch (e) {
|
|
84
|
-
console.error(`WARN ${id} planOff threw, so it stays planned: ${(e as Error).message}`)
|
|
85
|
-
return {}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
77
|
export type CheckRow = {
|
|
90
78
|
decl: CheckDecl
|
|
91
79
|
/** the display name an operator reads (its title, else the id) */
|
|
@@ -126,16 +114,16 @@ export type CheckRow = {
|
|
|
126
114
|
* then re-runs the check. `shipkit checks --fixable` is the catalog of
|
|
127
115
|
* everything in this repo that repairs itself. */
|
|
128
116
|
fixable: boolean
|
|
129
|
-
/** WHERE IT RUNS,
|
|
130
|
-
*
|
|
117
|
+
/** WHERE IT RUNS, every context resolved: what the check file declared,
|
|
118
|
+
* or what the code deriving the row wrote, with the repo's `off`
|
|
119
|
+
* override laid over it (core/runs.ts, core/when.ts) */
|
|
120
|
+
runs: ResolvedRuns
|
|
121
|
+
/** how its answer is kept, as one short cell (core/runs.ts cacheText) */
|
|
122
|
+
cache: string
|
|
123
|
+
/** the coarse word the runs read as, for the surfaces that show one */
|
|
131
124
|
when: WhenDecl
|
|
132
125
|
/** what it waits on, null when it waits on nothing */
|
|
133
126
|
deps: string[] | null
|
|
134
|
-
/** when the planner runs it on its own (check.ts `every`, `cron`,
|
|
135
|
-
* `whenInactive`). Absent: nothing declared, the planner's defaults */
|
|
136
|
-
plan?: PlanDecl
|
|
137
|
-
/** why the plan leaves this check out in this repo (check.ts `planOff`) */
|
|
138
|
-
planOff?: string
|
|
139
127
|
/** whose check it is: `shipkit` = it ships with the toolkit (a built-in
|
|
140
128
|
* SK check, the contracts pass, a floor slot nothing in the repo filled),
|
|
141
129
|
* `repo` = this repo declared it (a package.json script behind a floor
|
|
@@ -178,7 +166,7 @@ function runnableOf(row: CheckRow, body: CheckBody, o: { fix?: unknown; pointer?
|
|
|
178
166
|
owner: row.owner ?? row.decl.id,
|
|
179
167
|
blocking: row.blocking,
|
|
180
168
|
build: row.build,
|
|
181
|
-
|
|
169
|
+
runs: row.runs,
|
|
182
170
|
deps: row.deps ?? [],
|
|
183
171
|
group: row.group,
|
|
184
172
|
timeoutMs: row.timeoutMs ?? (row.build ? BUILD_TIMEOUT_MS : CHECK_TIMEOUT_MS),
|
|
@@ -211,6 +199,59 @@ function rootTestKey(root: string, c: RepoConfig, floor: FloorResolution): { inp
|
|
|
211
199
|
const taskDescription = (t: TaskDef): string =>
|
|
212
200
|
describeFirst(t.description, t.cmd == null ? null : `\`${t.cmd.join(' ')}\` exits 0`, `the ${t.id} task ${t.cmd == null ? 'passes' : 'exits 0'}`)
|
|
213
201
|
|
|
202
|
+
/** the default range a repo row nothing can key runs on, so it is asked
|
|
203
|
+
* about once a day and never in step with its siblings */
|
|
204
|
+
const DAILY: [string, string] = ['20h', '28h']
|
|
205
|
+
|
|
206
|
+
/** the runs of a row with no file to declare them in (a floor slot, a config
|
|
207
|
+
* task, a legacy rule or script), written out in full with the reason. It
|
|
208
|
+
* keeps what the planner and the gate did with such a row before the
|
|
209
|
+
* declaration was explicit: a ttl is the cadence ('once' for a forever
|
|
210
|
+
* fact), a save moves a keyed row or a fact, a gate build step nothing keys
|
|
211
|
+
* reruns after every save burst, and any other row nothing keys runs about
|
|
212
|
+
* once a day */
|
|
213
|
+
function derivedRuns(o: { gate: boolean; build: boolean; inputs?: string[]; ttl?: string; kind: string; repair: boolean }): ResolvedRuns {
|
|
214
|
+
const keyed = o.inputs != null
|
|
215
|
+
const ttl = o.ttl != null && o.ttl !== '' ? o.ttl : null
|
|
216
|
+
const fact = ttl === FOREVER
|
|
217
|
+
const parked = o.build && !o.gate
|
|
218
|
+
const onClock: Runs['onClock'] = parked ? false : fact ? 'once' : ttl != null ? { every: ttl, inactive: 'wait' } : o.build || keyed ? false : { every: DAILY, inactive: 'wait' }
|
|
219
|
+
const onSave: Runs['onSave'] = parked ? false : fact || (keyed && ttl == null) ? 'moved' : ttl == null && o.build ? 'burst' : false
|
|
220
|
+
const r: Runs = { ...NOWHERE, onCi: o.gate, onFull: o.gate, onRelease: o.gate, onHealth: !o.build, onSave, onClock }
|
|
221
|
+
const fix = !o.repair ? 'none' : o.gate ? 'ci' : 'manual'
|
|
222
|
+
const clock = onClock === false ? '' : onClock === 'once' ? ', once while its fact is unknown' : `, on the clock every ${ttl ?? DAILY.join('..')}`
|
|
223
|
+
const save = onSave === 'moved' ? ', the daemon when its key moves' : onSave === 'burst' ? ', the daemon after every save burst since nothing keys it' : ''
|
|
224
|
+
const where = parked ? 'parked: only --with or --only names it' : `${o.gate ? 'every gate' : 'no gate'}${o.build ? ', never the health pass' : ' and the health pass'}${save}${clock}`
|
|
225
|
+
return sourced(r, fix, 'derived', `${o.kind}: ${where}`)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** the cache half of a check file, as the key layer reads it */
|
|
229
|
+
function declOfCache(cache: CacheDecl, env: CheckEnv): { inputs?: string[]; ttl?: string; never?: string; key?: string; reads?: boolean } {
|
|
230
|
+
switch (cache.by) {
|
|
231
|
+
case 'files': return { inputs: cache.inputs, key: cache.key?.(env) }
|
|
232
|
+
case 'reads': return { reads: true }
|
|
233
|
+
case 'ttl': return { ttl: cache.span, key: cache.key?.(env) }
|
|
234
|
+
case 'fact': return { ttl: FOREVER, key: cache.key(env) }
|
|
235
|
+
case 'never': return { never: cache.why }
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** a legacy row's cache, as one cell */
|
|
240
|
+
const legacyCacheText = (d: { inputs?: string[]; ttl?: string; never?: string }): string =>
|
|
241
|
+
d.inputs != null ? filesText(d.inputs) : d.ttl != null && d.ttl !== '' ? (d.ttl === FOREVER ? 'fact' : `ttl ${d.ttl}`) : d.never != null ? `never: ${d.never}` : 'never: nothing declared'
|
|
242
|
+
|
|
243
|
+
/** a check file's own runs, resolved: its declaration, n/a where it does not
|
|
244
|
+
* apply, then the repo's override */
|
|
245
|
+
function fileRuns(def: CheckDef, id: string, c: RepoConfig, env: CheckEnv): ResolvedRuns {
|
|
246
|
+
const own = sourced(def.runs, def.fix, 'check file')
|
|
247
|
+
let na: string | null = null
|
|
248
|
+
if (typeof def.applies === 'function') {
|
|
249
|
+
try { na = def.applies(env) } catch (e) { console.error(`WARN ${id} applies threw, so it applies: ${(e as Error).message}`) }
|
|
250
|
+
}
|
|
251
|
+
const base = na == null ? own : { ...own, off: `n/a here: ${na}` }
|
|
252
|
+
return withRepoOff(base, repoOff(c, id))
|
|
253
|
+
}
|
|
254
|
+
|
|
214
255
|
/** THE enumeration. Everything below reads it, nothing else walks. */
|
|
215
256
|
export async function composeChecks(root: string, c: RepoConfig, o: ComposeOpts = {}): Promise<GateCheck[]> {
|
|
216
257
|
const out: GateCheck[] = []
|
|
@@ -252,10 +293,13 @@ export async function composeChecks(root: string, c: RepoConfig, o: ComposeOpts
|
|
|
252
293
|
for (const raw of [...floor.tasks, ...(c.tasks ?? []).filter((t) => foldedInto.get(t.id) !== t)]) {
|
|
253
294
|
const t: TaskDef = full ? { ...raw, env: { ...raw.env, [FULL_TESTS_ENV]: '1' } } : raw
|
|
254
295
|
const own = t.id === 'test' && floor.tasks.includes(raw) ? rootTest : null
|
|
296
|
+
const inputs = own?.inputs ?? t.inputs
|
|
297
|
+
const kind = floor.tasks.includes(raw) ? 'a floor slot' : 'a repo.config.ts task'
|
|
298
|
+
const runs = withRepoOff(derivedRuns({ gate: t.optIn !== true, build: true, inputs, ttl: t.ttl, kind, repair: false }), repoOff(c, t.id))
|
|
255
299
|
const row: CheckRow = {
|
|
256
300
|
decl: {
|
|
257
301
|
id: t.id,
|
|
258
|
-
inputs
|
|
302
|
+
inputs,
|
|
259
303
|
ttl: t.ttl,
|
|
260
304
|
never: t.never,
|
|
261
305
|
argv: t.cmd,
|
|
@@ -266,7 +310,7 @@ export async function composeChecks(root: string, c: RepoConfig, o: ComposeOpts
|
|
|
266
310
|
description: taskDescription(t),
|
|
267
311
|
// an optIn task is off the default run, which is the same shape as
|
|
268
312
|
// a check that opted out of the gate
|
|
269
|
-
gate:
|
|
313
|
+
gate: runs.off == null && runs.onCi,
|
|
270
314
|
build: true,
|
|
271
315
|
// a build step IS the build: it has no non-blocking mode
|
|
272
316
|
blocking: true,
|
|
@@ -278,7 +322,9 @@ export async function composeChecks(root: string, c: RepoConfig, o: ComposeOpts
|
|
|
278
322
|
// a build step runs a TOOL, and repairing what a tool reports is
|
|
279
323
|
// that tool's own job (a formatter, a codemod), never the gate's
|
|
280
324
|
fixable: false,
|
|
281
|
-
|
|
325
|
+
runs,
|
|
326
|
+
cache: legacyCacheText({ inputs, ttl: t.ttl, never: t.never }),
|
|
327
|
+
when: whenSummary(runs, repoOff(c, t.id), false),
|
|
282
328
|
deps: t.deps ?? null,
|
|
283
329
|
from: taskFrom(t.id),
|
|
284
330
|
}
|
|
@@ -293,6 +339,7 @@ export async function composeChecks(root: string, c: RepoConfig, o: ComposeOpts
|
|
|
293
339
|
// no command, which is why it declares `never` rather than joining the
|
|
294
340
|
// burn-down of checks nobody has keyed yet.
|
|
295
341
|
for (const id of floor.disabled) {
|
|
342
|
+
const runs = sourced(NOWHERE, 'none', 'repo.config.ts', null, `defaults.${id}: false`)
|
|
296
343
|
const row: CheckRow = {
|
|
297
344
|
decl: { id, never: `turned off by defaults.${id}: false, so there is no command to key` },
|
|
298
345
|
title: id,
|
|
@@ -306,7 +353,9 @@ export async function composeChecks(root: string, c: RepoConfig, o: ComposeOpts
|
|
|
306
353
|
budgetMs: budgetOf(id, undefined, { build: true, cmd: false, world: false }, c.budgets),
|
|
307
354
|
optIn: false,
|
|
308
355
|
fixable: false,
|
|
309
|
-
|
|
356
|
+
runs,
|
|
357
|
+
cache: `never: turned off by defaults.${id}: false`,
|
|
358
|
+
when: whenSummary(runs, { reason: `defaults.${id}: false`, source: 'defaults' }, false),
|
|
310
359
|
deps: null,
|
|
311
360
|
// the repo wrote the line that turned it off
|
|
312
361
|
from: 'repo',
|
|
@@ -319,46 +368,44 @@ export async function composeChecks(root: string, c: RepoConfig, o: ComposeOpts
|
|
|
319
368
|
const configs = new Map<string, RvtConfig>()
|
|
320
369
|
const texts = new Map<string, string>()
|
|
321
370
|
for (const u of checks) {
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
// through its context (git-key.ts
|
|
325
|
-
// question: a body can read the env,
|
|
326
|
-
// stand in for a full one
|
|
327
|
-
|
|
328
|
-
//
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
const reads = undeclared && raw == null
|
|
332
|
-
const extra = checkCacheExtra({ id: u.id, cacheKey: u.def.cacheKey }, env, w)
|
|
371
|
+
const runs = fileRuns(u.def, u.id, c, env)
|
|
372
|
+
const cache = declOfCache(u.def.cache, env)
|
|
373
|
+
// a body keyed on what it reads through its context (git-key.ts
|
|
374
|
+
// READS). The tier is part of that question: a body can read the env,
|
|
375
|
+
// and a quick answer must never stand in for a full one. Any module
|
|
376
|
+
// its body can reach that touches the disk or a process around `c`
|
|
377
|
+
// contradicts the declaration (core/raw-access.ts), and says so loud
|
|
378
|
+
const raw = cache.reads === true && u.def.run != null ? rawAccessOf(root, u, configs, texts) : null
|
|
379
|
+
const extra = checkCacheExtra(cache.key, runs.off)
|
|
333
380
|
const row: CheckRow = {
|
|
334
381
|
decl: {
|
|
335
382
|
id: u.id,
|
|
336
|
-
inputs:
|
|
383
|
+
inputs: cache.inputs,
|
|
337
384
|
code: u.rel == null ? undefined : [u.rel],
|
|
338
|
-
ttl:
|
|
339
|
-
never:
|
|
385
|
+
ttl: cache.ttl,
|
|
386
|
+
never: cache.never,
|
|
340
387
|
argv: u.def.cmd,
|
|
341
|
-
extra: reads ? `${extra ?? ''}|tier=${full ? 'full' : 'quick'}` : extra,
|
|
388
|
+
extra: cache.reads === true ? `${extra ?? ''}|tier=${full ? 'full' : 'quick'}` : extra,
|
|
342
389
|
writes: u.def.writes,
|
|
343
|
-
...(reads ? { reads: true } : {}),
|
|
344
|
-
...(raw != null ? {
|
|
390
|
+
...(cache.reads === true && raw == null ? { reads: true } : {}),
|
|
391
|
+
...(raw != null ? { declError: `declares cache reads, but ${raw}` } : {}),
|
|
345
392
|
},
|
|
346
393
|
title: u.title,
|
|
347
394
|
description: u.description,
|
|
348
|
-
gate:
|
|
395
|
+
gate: runs.off == null && runs.onCi,
|
|
349
396
|
build: u.def.build === true,
|
|
350
397
|
blocking: u.def.blocking,
|
|
351
398
|
owner: u.owner,
|
|
352
399
|
group: u.def.group ?? null,
|
|
353
400
|
timeoutMs: u.def.timeoutMs ?? null,
|
|
354
|
-
budgetMs: budgetOf(u.id, u.def.budgetMs, { build: u.def.build === true, cmd: u.def.cmd != null, world:
|
|
401
|
+
budgetMs: budgetOf(u.id, u.def.budgetMs, { build: u.def.build === true, cmd: u.def.cmd != null, world: cache.ttl != null || cache.never != null }, c.budgets),
|
|
355
402
|
optIn: false,
|
|
356
|
-
fixable: u.def.
|
|
357
|
-
|
|
403
|
+
fixable: u.def.repair != null,
|
|
404
|
+
runs,
|
|
405
|
+
cache: cacheText(u.def.cache),
|
|
406
|
+
when: whenSummary(runs, repoOff(c, u.id), true),
|
|
358
407
|
deps: u.def.deps ?? null,
|
|
359
408
|
from: u.from,
|
|
360
|
-
...(u.def.every !== undefined || u.def.cron != null || u.def.whenInactive != null ? { plan: { every: u.def.every, cron: u.def.cron, whenInactive: u.def.whenInactive } } : {}),
|
|
361
|
-
...planOffOf(u.id, u.def.planOff, env),
|
|
362
409
|
}
|
|
363
410
|
const body: CheckBody = {
|
|
364
411
|
...(u.def.cmd == null ? {} : { cmd: u.def.cmd }),
|
|
@@ -367,11 +414,12 @@ export async function composeChecks(root: string, c: RepoConfig, o: ComposeOpts
|
|
|
367
414
|
// relative to the root, and an absolute one stands as written
|
|
368
415
|
...(u.def.cwd == null ? {} : { cwd: resolve(root, u.def.cwd) }),
|
|
369
416
|
}
|
|
370
|
-
out.push({ row, runnable: runnableOf(row, body, { fix: u.def.
|
|
417
|
+
out.push({ row, runnable: runnableOf(row, body, { fix: u.def.repair, pointer: u.def.cmd?.join(' ') ?? null }) })
|
|
371
418
|
}
|
|
372
419
|
// LEGACY: `rules: []`, read as a check whose body is its lint. Dies with
|
|
373
420
|
// that config key once `shipkit fleet` shows no repo below 0.40.
|
|
374
421
|
for (const r of c.rules ?? []) {
|
|
422
|
+
const runs = withRepoOff(derivedRuns({ gate: true, build: false, inputs: r.inputs, ttl: r.ttl, kind: 'a legacy repo.config.ts rule', repair: r.fix != null }), repoOff(c, r.id))
|
|
375
423
|
const row: CheckRow = {
|
|
376
424
|
decl: {
|
|
377
425
|
id: r.id,
|
|
@@ -383,7 +431,7 @@ export async function composeChecks(root: string, c: RepoConfig, o: ComposeOpts
|
|
|
383
431
|
},
|
|
384
432
|
title: r.title,
|
|
385
433
|
description: describeFirst(r.title, `rule ${r.id} holds`),
|
|
386
|
-
gate:
|
|
434
|
+
gate: runs.off == null && runs.onCi,
|
|
387
435
|
build: false,
|
|
388
436
|
blocking: r.blocking,
|
|
389
437
|
owner: r.owner,
|
|
@@ -392,7 +440,9 @@ export async function composeChecks(root: string, c: RepoConfig, o: ComposeOpts
|
|
|
392
440
|
budgetMs: budgetOf(r.id, undefined, { build: false, cmd: false, world: r.ttl != null || r.never != null }, c.budgets),
|
|
393
441
|
optIn: false,
|
|
394
442
|
fixable: r.fix != null,
|
|
395
|
-
|
|
443
|
+
runs,
|
|
444
|
+
cache: legacyCacheText(r),
|
|
445
|
+
when: whenSummary(runs, repoOff(c, r.id), false),
|
|
396
446
|
deps: null,
|
|
397
447
|
from: 'repo',
|
|
398
448
|
}
|
|
@@ -409,12 +459,14 @@ export async function composeChecks(root: string, c: RepoConfig, o: ComposeOpts
|
|
|
409
459
|
// file's own cache declaration wins over the task's
|
|
410
460
|
const d = task == null ? found : { ...found, gate: found.gate || task.optIn !== true }
|
|
411
461
|
const cached = d.inputs != null || d.ttl != null || d.never != null
|
|
462
|
+
const li = { inputs: d.inputs ?? (cached ? undefined : task?.inputs), ttl: d.ttl ?? (cached ? undefined : task?.ttl), never: d.never ?? (cached ? undefined : task?.never) }
|
|
463
|
+
const runs = withRepoOff(derivedRuns({ gate: d.gate, build: false, inputs: li.inputs, ttl: li.ttl, kind: 'a legacy check script', repair: false }), repoOff(c, d.name))
|
|
412
464
|
const row: CheckRow = {
|
|
413
465
|
decl: {
|
|
414
466
|
id: d.name,
|
|
415
|
-
inputs:
|
|
416
|
-
ttl:
|
|
417
|
-
never:
|
|
467
|
+
inputs: li.inputs,
|
|
468
|
+
ttl: li.ttl,
|
|
469
|
+
never: li.never,
|
|
418
470
|
code: [rel],
|
|
419
471
|
argv: ['bun', rel],
|
|
420
472
|
// no `extra`, so WHERE it runs stays out of a legacy check's
|
|
@@ -425,7 +477,7 @@ export async function composeChecks(root: string, c: RepoConfig, o: ComposeOpts
|
|
|
425
477
|
},
|
|
426
478
|
title: d.name,
|
|
427
479
|
description: describeFirst(`\`bun ${rel}\` exits 0`, `${d.name} exits 0`),
|
|
428
|
-
gate:
|
|
480
|
+
gate: runs.off == null && runs.onCi,
|
|
429
481
|
build: false,
|
|
430
482
|
// the file wins, then the repo default, then nothing: an
|
|
431
483
|
// unclassified check is an error, never a guess
|
|
@@ -437,7 +489,9 @@ export async function composeChecks(root: string, c: RepoConfig, o: ComposeOpts
|
|
|
437
489
|
optIn: false,
|
|
438
490
|
// the reader is a source scanner, so it cannot carry a function
|
|
439
491
|
fixable: false,
|
|
440
|
-
|
|
492
|
+
runs,
|
|
493
|
+
cache: legacyCacheText(li),
|
|
494
|
+
when: whenSummary(runs, repoOff(c, d.name), false),
|
|
441
495
|
deps: task?.deps ?? null,
|
|
442
496
|
from: 'repo',
|
|
443
497
|
}
|
package/src/ledger/git-key.ts
CHANGED
|
@@ -85,6 +85,9 @@ export type CheckDecl = {
|
|
|
85
85
|
/** why an undeclared `run` check stays uncached: the first module its body
|
|
86
86
|
* can reach that touches the disk or a process around the context */
|
|
87
87
|
uncacheable?: string
|
|
88
|
+
/** the check's cache declaration contradicts its body (`reads` on a body
|
|
89
|
+
* that touches the disk around its context): a registration failure */
|
|
90
|
+
declError?: string
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
export type CheckKey = {
|
|
@@ -121,6 +124,7 @@ export type Registration = {
|
|
|
121
124
|
* `inputs` on a `ttl` check is a registration FAIL: a check cannot both be
|
|
122
125
|
* described by files on disk and be un-describable by them. */
|
|
123
126
|
export function classify(d: CheckDecl): { population: Population; error?: string; reason?: string; decided?: boolean } {
|
|
127
|
+
if (d.declError != null) return { population: 'none', error: `check '${d.id}': ${d.declError}` }
|
|
124
128
|
const hasInputs = (d.inputs ?? []).length > 0
|
|
125
129
|
const hasTtl = d.ttl != null && d.ttl !== ''
|
|
126
130
|
const hasNever = d.never != null && d.never !== ''
|
|
@@ -230,7 +234,7 @@ export class KeyLayer {
|
|
|
230
234
|
// no key at all
|
|
231
235
|
let toolchain: { id: string; uncacheable: string[] }
|
|
232
236
|
try {
|
|
233
|
-
toolchain = toolchainIdentity(root, o.cliEntry)
|
|
237
|
+
toolchain = { ...toolchainIdentity(root, o.cliEntry), uncacheable: [] }
|
|
234
238
|
} catch (e) {
|
|
235
239
|
toolchain = { id: 'unresolved', uncacheable: [`gate binary unreadable at ${o.cliEntry}: ${e instanceof Error ? e.message : String(e)}`] }
|
|
236
240
|
}
|
package/src/ledger/ledger.ts
CHANGED
|
@@ -156,7 +156,7 @@ export class Ledger {
|
|
|
156
156
|
return k
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
private read(k: CheckKey): { payload: Stored } | null {
|
|
159
|
+
private read(k: CheckKey): { payload: Stored; at: number } | null {
|
|
160
160
|
return this.answers.read<Stored>(NS, k.id, k.key!)
|
|
161
161
|
}
|
|
162
162
|
|
|
@@ -168,7 +168,7 @@ export class Ledger {
|
|
|
168
168
|
|
|
169
169
|
/** the stored answer for this check, or null when it must run. Counts what
|
|
170
170
|
* it was asked, so the run can report hit/miss/uncacheable honestly. */
|
|
171
|
-
replay(id: string): CheckOutcome | null {
|
|
171
|
+
replay(id: string): (CheckOutcome & { at: number }) | null {
|
|
172
172
|
const k = this.key(id)
|
|
173
173
|
if (k.key == null) { this.unkeyed++; return null }
|
|
174
174
|
if (!this.reading) { this.misses++; return null }
|
|
@@ -177,7 +177,7 @@ export class Ledger {
|
|
|
177
177
|
if (k.population === 'reads' && (e.payload.reads == null || this.driftOf(e.payload.reads, () => this.view()) != null)) { this.misses++; return null }
|
|
178
178
|
this.hits++
|
|
179
179
|
this.replayed.push({ ns: NS, name: k.id, key: k.key })
|
|
180
|
-
return e.payload
|
|
180
|
+
return { ...e.payload, at: e.at }
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
/** the stored read set of a READS check, as the last store wrote it */
|
|
@@ -305,6 +305,6 @@ export function inflightSlot(dir: string, ns: string, id: string, key: string):
|
|
|
305
305
|
/** stamp a cached check's lines. The FIRST line carries the word, because
|
|
306
306
|
* that is the line an agent reads, and a cached answer that renders
|
|
307
307
|
* identically to one that ran is the one thing this design refuses to ship. */
|
|
308
|
-
export function stampCached(lines: Line[], measuredMs: number): Line[] {
|
|
309
|
-
return lines.map((l, i) => (i === 0 ? { ...l, text: `${l.text} ${cachedStamp(measuredMs)}` } : l))
|
|
308
|
+
export function stampCached(lines: Line[], measuredMs: number, at: number, now: number = Date.now()): Line[] {
|
|
309
|
+
return lines.map((l, i) => (i === 0 ? { ...l, text: `${l.text} ${cachedStamp(measuredMs, now - at)}` } : l))
|
|
310
310
|
}
|