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
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.
|
|
@@ -53,7 +58,8 @@ export type HooksConfig = {
|
|
|
53
58
|
languageAllow?: string[]
|
|
54
59
|
/** disable a built-in gate, visible in `hooks list`, never silent.
|
|
55
60
|
* 'commit-language' lifts ONLY the French check, keeping the dash and
|
|
56
|
-
* AI-attribution guards armed.
|
|
61
|
+
* AI-attribution guards armed. 'commit-message' lifts the dash and French
|
|
62
|
+
* checks; the AI-attribution guard is never lifted. */
|
|
57
63
|
disable?: ('leak-guard' | 'commit-message' | 'root-identity' | 'commit-language')[]
|
|
58
64
|
}
|
|
59
65
|
|
|
@@ -209,7 +215,7 @@ export function blockedStaged(staged: string[], config: RepoConfig): string[] {
|
|
|
209
215
|
const draftsOpen = (c: RepoConfig): boolean => c.visibility === 'private'
|
|
210
216
|
|
|
211
217
|
/** the pre-commit gate: built-in leak guard, then the repo's own commands.
|
|
212
|
-
* 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
|
|
213
219
|
* over budget WARNs naming the fix, so drift back to slow commits is loud. */
|
|
214
220
|
export async function runPreCommit(root: string, config: RepoConfig): Promise<GateOutcome[]> {
|
|
215
221
|
const started = performance.now()
|
|
@@ -272,6 +278,7 @@ export async function runPreCommit(root: string, config: RepoConfig): Promise<Ga
|
|
|
272
278
|
],
|
|
273
279
|
})
|
|
274
280
|
}
|
|
281
|
+
out.push(...(await runPreCommitChecks(root, config)))
|
|
275
282
|
for (const c of h.preCommit ?? []) {
|
|
276
283
|
const r = await spawnWithTimeout(c.cmd, { cwd: root, timeoutMs: 120_000 })
|
|
277
284
|
out.push({
|
|
@@ -281,15 +288,52 @@ export async function runPreCommit(root: string, config: RepoConfig): Promise<Ga
|
|
|
281
288
|
})
|
|
282
289
|
}
|
|
283
290
|
const elapsed = performance.now() - started
|
|
284
|
-
if (elapsed >
|
|
291
|
+
if (elapsed > HOOK_BUDGET_MS)
|
|
285
292
|
out.push({
|
|
286
293
|
id: 'hook-budget',
|
|
287
294
|
ok: true,
|
|
288
|
-
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)`],
|
|
289
296
|
})
|
|
290
297
|
return out
|
|
291
298
|
}
|
|
292
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
|
+
|
|
293
337
|
/** the commit-msg gate: the shared message predicate, then the repo's own. */
|
|
294
338
|
export async function runCommitMsg(root: string, config: RepoConfig, messageFile: string): Promise<GateOutcome[]> {
|
|
295
339
|
const h = config.hooks ?? {}
|
|
@@ -309,6 +353,13 @@ export async function runCommitMsg(root: string, config: RepoConfig, messageFile
|
|
|
309
353
|
...offenses.map((o) => ` line ${o.line}: ${o.text.slice(0, 100)}\n ${o.why}`),
|
|
310
354
|
],
|
|
311
355
|
})
|
|
356
|
+
} else {
|
|
357
|
+
// AI attribution is the one message rule no config lifts: this hook is
|
|
358
|
+
// the guard against it in every repo (identity/how-we-commit.md)
|
|
359
|
+
const offenses = commitMessageOffenses(message).filter((o) => o.kind === 'attribution')
|
|
360
|
+
out.push(offenses.length === 0
|
|
361
|
+
? { id: 'ai-attribution', ok: true, lines: ['ok: no AI attribution in the commit message'] }
|
|
362
|
+
: { id: 'ai-attribution', ok: false, lines: [`FAIL: commit message carries AI attribution`, ...offenses.map((o) => ` line ${o.line}: ${o.text.slice(0, 100)}\n ${o.why}`)] })
|
|
312
363
|
}
|
|
313
364
|
// the leak guard also reads the MESSAGE, a private name pasted into a
|
|
314
365
|
// commit body is as public as one pasted into a file
|
|
@@ -331,7 +382,7 @@ export async function runCommitMsg(root: string, config: RepoConfig, messageFile
|
|
|
331
382
|
}
|
|
332
383
|
|
|
333
384
|
/** what blocks a commit here, the enumeration hand-rolled hooks never had. */
|
|
334
|
-
export function listGates(config: RepoConfig, root = process.cwd()): string[] {
|
|
385
|
+
export function listGates(config: RepoConfig, root = process.cwd(), checks: { id: string; budgetMs: number }[] = []): string[] {
|
|
335
386
|
const h = config.hooks ?? {}
|
|
336
387
|
const off = new Set(h.disable ?? [])
|
|
337
388
|
const kw = h.keywordFile ?? KEYWORD_FILE
|
|
@@ -346,8 +397,11 @@ export function listGates(config: RepoConfig, root = process.cwd()): string[] {
|
|
|
346
397
|
`pre-commit leak-guard${leakState} — staged added lines + paths vs ${kw} + ~/.shipkit/private/banned-keywords.txt${leakAllowState}`,
|
|
347
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)` : ''}`] : []),
|
|
348
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`),
|
|
349
401
|
...(h.preCommit ?? []).map((c) => `pre-commit ${c.id} — ${c.cmd.join(' ')}`),
|
|
350
|
-
|
|
402
|
+
off.has('commit-message')
|
|
403
|
+
? `commit-msg commit-message (DISABLED by config) — dash punctuation + French; ai-attribution stays on, never disabled`
|
|
404
|
+
: `commit-msg commit-message — dash punctuation + AI attribution (never disabled) + French${off.has('commit-language') ? ' (commit-language DISABLED by config)' : ''}`,
|
|
351
405
|
`commit-msg leak-guard${leakState} — the message text`,
|
|
352
406
|
...(h.commitMsg ?? []).map((c) => `commit-msg ${c.id} — ${c.cmd.join(' ')}`),
|
|
353
407
|
]
|
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,16 +431,18 @@ 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,
|
|
390
438
|
group: null,
|
|
391
439
|
timeoutMs: null,
|
|
392
|
-
budgetMs: budgetOf(r.id, undefined, { build: false, cmd: false, world: r.ttl != null }, c.budgets),
|
|
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
|
|
@@ -433,11 +485,13 @@ export async function composeChecks(root: string, c: RepoConfig, o: ComposeOpts
|
|
|
433
485
|
owner: rel,
|
|
434
486
|
group: task?.group ?? null,
|
|
435
487
|
timeoutMs: task?.timeoutMs ?? null,
|
|
436
|
-
budgetMs: budgetOf(d.name, undefined, { build: false, cmd: true, world: d.ttl != null }, c.budgets),
|
|
488
|
+
budgetMs: budgetOf(d.name, undefined, { build: false, cmd: true, world: d.ttl != null || d.never != null }, c.budgets),
|
|
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
|
}
|
package/src/ledger/store.ts
CHANGED
|
@@ -105,6 +105,17 @@ export class Answers {
|
|
|
105
105
|
this.h = null
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/** the busy timeout sqlite holds on the open handle, in ms: how long its
|
|
109
|
+
* last statement could wait on a writer. Null with no handle */
|
|
110
|
+
busyTimeout(): number | null {
|
|
111
|
+
try {
|
|
112
|
+
const r = this.h?.query('pragma busy_timeout').get() as Record<string, number> | null | undefined
|
|
113
|
+
return r == null ? null : (Object.values(r)[0] ?? null)
|
|
114
|
+
} catch {
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
108
119
|
/** the stored answer, or null when the check must run */
|
|
109
120
|
read<T>(ns: string, name: string, key: string): Stored<T> | null {
|
|
110
121
|
const row = this.with(READ_BUSY_MS, (h) => h.query('select v, at, keeps, payload from answers where ns = ? and name = ? and key = ?').get(ns, name, key) as Row | null)
|