rvlib-shipkit 1.4.0 → 1.6.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 +114 -0
- package/README.md +17 -17
- package/package.json +3 -2
- package/src/assets/gen/comfyts-gen.ts +12 -8
- package/src/assets/server/studio-server.ts +14 -2
- package/src/check.ts +73 -103
- package/src/checks/SK001-package-json-shape.check.ts +11 -6
- package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
- package/src/checks/SK003-claude-md-shape.check.ts +7 -3
- 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/checks/SK030-cache-audit.check.ts +34 -0
- package/src/checks/SK031-trace-audit.check.ts +39 -0
- package/src/checks/SK032-ignored-inputs.check.ts +79 -0
- package/src/cli/cmd_Asks.ts +5 -5
- package/src/cli/cmd_Assets.ts +17 -13
- package/src/cli/cmd_Bump.ts +12 -11
- package/src/cli/cmd_CacheAudit.ts +43 -0
- 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 +8 -6
- package/src/cli/cmd_Fleet.ts +9 -7
- package/src/cli/cmd_GlobalProcesses.ts +120 -7
- package/src/cli/cmd_Home.ts +26 -19
- package/src/cli/cmd_Hooks.ts +6 -5
- package/src/cli/cmd_Init.ts +5 -4
- package/src/cli/cmd_Keys.ts +20 -8
- package/src/cli/cmd_Mv.ts +7 -6
- package/src/cli/cmd_Needs.ts +4 -3
- package/src/cli/cmd_Overview.ts +5 -4
- 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 +6 -4
- 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_TraceAudit.ts +62 -0
- 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 +11 -2
- package/src/collections/checks.ts +2 -0
- package/src/collections/collection-types.ts +12 -4
- package/src/collections/providers.ts +5 -3
- package/src/core/app-icon.ts +154 -0
- package/src/core/audit-report.ts +21 -0
- package/src/core/cache-audit.ts +72 -0
- package/src/core/canonical-scripts.ts +2 -2
- package/src/core/check-ctx.ts +14 -5
- package/src/core/check-runner.ts +43 -23
- package/src/core/config-rewrite.ts +37 -24
- package/src/core/discover-checks.ts +23 -2
- package/src/core/floor-resolve.ts +5 -7
- package/src/core/git-root.ts +13 -0
- package/src/core/ignored-block.ts +94 -0
- package/src/core/ignored-inputs.ts +47 -0
- package/src/core/jobs.ts +9 -2
- package/src/core/legacy-shapes.ts +39 -10
- package/src/core/one-lib.ts +59 -4
- 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/read-set.ts +97 -9
- package/src/core/retention.ts +0 -14
- package/src/core/run-check.ts +12 -9
- package/src/core/run-ci.ts +14 -5
- package/src/core/run-fix.ts +30 -23
- package/src/core/run-fixers.ts +48 -5
- package/src/core/runs-matrix.ts +179 -0
- package/src/core/runs.ts +242 -0
- package/src/core/trace-audit.ts +120 -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/gui-app.ts +24 -27
- package/src/home/home-launchd.ts +20 -4
- package/src/home/home-routes.ts +12 -1
- package/src/home/home-start.ts +11 -5
- package/src/home/hubs.ts +5 -5
- package/src/home/open-app.ts +2 -2
- 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/home/via-hub.ts +14 -0
- package/src/hooks/run-hooks.ts +48 -4
- package/src/init-repo.ts +6 -6
- package/src/ledger/db.ts +2 -13
- package/src/ledger/gate-checks.ts +133 -61
- package/src/ledger/git-key.ts +153 -31
- package/src/ledger/git-scan.ts +5 -2
- package/src/ledger/ledger.ts +136 -21
- package/src/ledger/store.ts +14 -0
- package/src/ledger/toolchain-identity.ts +248 -45
- package/src/plan/repo-source.ts +18 -39
- package/src/problems/extract.ts +3 -0
- package/src/release/run-release.ts +5 -0
- package/src/repo-config.ts +46 -54
- 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-svc.ts +36 -3
- package/src/serve/hub-app/admin-view.tsx +46 -12
- 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-card.tsx +1 -44
- package/src/serve/hub-app/col-tables-view.tsx +84 -11
- package/src/serve/hub-app/col-tables.ts +27 -0
- 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 -12
- package/src/serve/hub-app/home-strip.tsx +115 -64
- package/src/serve/hub-app/home.html +1 -0
- package/src/serve/hub-app/hub-app.tsx +32 -8
- package/src/serve/hub-app/hub-types.ts +5 -2
- package/src/serve/hub-app/hub.css +89 -63
- 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 +15 -4
- 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 +45 -0
- package/src/serve/hub-app/project-list.css +4 -1
- package/src/serve/hub-app/project-list.tsx +6 -3
- package/src/serve/hub-app/projects-svc.ts +25 -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 +131 -0
- package/src/serve/hub-app/repo-st.ts +70 -14
- package/src/serve/hub-app/sidebar.tsx +17 -42
- package/src/serve/hub-app/timeline-svc.ts +3 -2
- package/src/serve/hub-app/tree-build.ts +151 -155
- package/src/serve/hub-app/tree-row.tsx +55 -12
- package/src/serve/hub-app/tree-svc.ts +97 -56
- 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 +7 -18
- package/src/serve/project-gutter-route.ts +22 -1
- package/src/serve/run-serve.ts +1 -1
- package/src/serve/serve-hub.ts +4 -14
- package/src/suite/judge-suites.ts +6 -0
- package/src/trace/interpose.c +655 -0
- package/src/trace/read-trace.ts +485 -0
- package/src/trace/tracer.ts +282 -0
- 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
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// what it read: package.json, the probes below, the test file listing, and
|
|
6
6
|
// whatever the floor resolution read.
|
|
7
7
|
//
|
|
8
|
-
// It also enforces the package.json half of ONE WAY IN (build/shipkit.md):
|
|
8
|
+
// It also enforces the package.json half of ONE WAY IN (build/shipkit/one-way-in.md):
|
|
9
9
|
// `test`, `lint` and `typecheck` ROUTE to `shipkit ci --only <id>` and the
|
|
10
10
|
// tool body they used to hold sits at `<id>:raw`. That is what makes a cache
|
|
11
11
|
// worth having: `bun run test` is the reflex, and the reflex has to land in
|
|
@@ -125,13 +125,14 @@ export function evaluatePackageJson(t: CheckCtx): void {
|
|
|
125
125
|
//
|
|
126
126
|
// a shipkit verb is never on this list. `shipkit ci`, `check`, `contracts`,
|
|
127
127
|
// `tui`, `mv`, `assets`, `asks` are typed as themselves, and a package.json
|
|
128
|
-
// row that only forwards to one is a second name for one capability
|
|
128
|
+
// row that only forwards to one is a second name for one capability. nor is
|
|
129
|
+
// a global process (ComfyUI, TTS): `shipkit gp start <id>` starts it from any
|
|
130
|
+
// repo, so a per repo script would only copy it
|
|
129
131
|
function requiredScripts(c: CheckCtx): Array<{ name: string; why: string }> {
|
|
130
132
|
const required: Array<{ name: string; why: string }> = [
|
|
131
133
|
// watch-mode serve — the ONE shipkit surface the bare verb cannot give
|
|
132
134
|
{ name: 'hub', why: 'shipkit repo' },
|
|
133
135
|
]
|
|
134
|
-
if (c.exists('.shipkit/comfy')) required.push({ name: 'comfy:up', why: '.shipkit/comfy/ exists' })
|
|
135
136
|
if (c.exists('deploy.sh')) required.push({ name: 'up', why: 'deploy.sh exists' })
|
|
136
137
|
return required
|
|
137
138
|
}
|
|
@@ -197,12 +198,16 @@ export const packageJsonShape = check({
|
|
|
197
198
|
title: 'package.json',
|
|
198
199
|
description: 'package.json routes lint, typecheck and test through the gate',
|
|
199
200
|
owner: OWNER,
|
|
201
|
+
runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
|
|
202
|
+
cache: { by: 'reads' },
|
|
203
|
+
fix: 'ci',
|
|
204
|
+
applies: 'always',
|
|
200
205
|
run: (c) => evaluatePackageJson(c),
|
|
201
206
|
// mechanical repairs only: field flips + scripts whose canonical body the
|
|
202
|
-
// vocabulary page states.
|
|
203
|
-
//
|
|
207
|
+
// vocabulary page states. A body it does NOT state is named in the
|
|
208
|
+
// summary, never invented. rvlib-* roots untouched — their
|
|
204
209
|
// license/private are publish decisions.
|
|
205
|
-
|
|
210
|
+
repair: (c): FixResult => {
|
|
206
211
|
const raw = c.read('package.json')
|
|
207
212
|
if (raw == null) return { changed: false, summary: 'no package.json — nothing to fix' }
|
|
208
213
|
if (rootPublishesNpm(c.config)) return { changed: false, summary: 'published to npm: its package.json is its public contract, never rewritten' }
|
|
@@ -16,6 +16,10 @@ export const contractsFulfillness = check({
|
|
|
16
16
|
owner: 'meta/contracts.md',
|
|
17
17
|
// keyed on what the contracts read: each artifact, each detector, and each
|
|
18
18
|
// folder a glob walked through `c.entries`, a gitignored one included
|
|
19
|
+
runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
|
|
20
|
+
cache: { by: 'reads' },
|
|
21
|
+
fix: 'none',
|
|
22
|
+
applies: 'always',
|
|
19
23
|
run: async (c) => {
|
|
20
24
|
applyContractsResult(c, await c.use(contractsResult), contractsFulfillness)
|
|
21
25
|
},
|
|
@@ -18,8 +18,12 @@ export const claudeMdShape = check({
|
|
|
18
18
|
title: 'CLAUDE.md',
|
|
19
19
|
description: 'CLAUDE.md loads the house wiki into every session',
|
|
20
20
|
owner: 'meta/agent-folder.md',
|
|
21
|
-
// keyed on what it reads: CLAUDE.md
|
|
22
|
-
//
|
|
21
|
+
// keyed on what it reads: CLAUDE.md, whether agent/index.md exists, and
|
|
22
|
+
// the ignore files git read for the gitignore probe (c.exec runs traced)
|
|
23
|
+
runs: { onPreCommit: { budgetMs: 40 }, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
|
|
24
|
+
cache: { by: 'reads' },
|
|
25
|
+
fix: 'ci',
|
|
26
|
+
applies: 'always',
|
|
23
27
|
run: async (c) => {
|
|
24
28
|
const hasAgent = c.exists('agent/index.md')
|
|
25
29
|
const claude = c.read('CLAUDE.md')
|
|
@@ -53,7 +57,7 @@ export const claudeMdShape = check({
|
|
|
53
57
|
// CLAUDE.md would lint green with the base include forever missing,
|
|
54
58
|
// trading a loud FAIL for a permanent silent gap. Creation and prose
|
|
55
59
|
// relocation stay human/machine-local (`shipkit start --include`).
|
|
56
|
-
|
|
60
|
+
repair: (c): FixResult => {
|
|
57
61
|
if (!c.exists('agent/index.md')) return { changed: false, summary: 'no agent/index.md — nothing mechanical to fix' }
|
|
58
62
|
const cur = c.read('CLAUDE.md')
|
|
59
63
|
if (cur == null) return { changed: false, summary: 'CLAUDE.md missing — creation needs the base include (shipkit start <name> writes it), not autofixable' }
|
|
@@ -205,11 +205,14 @@ export const gitignorePolicy = check({
|
|
|
205
205
|
title: '.gitignore',
|
|
206
206
|
description: 'the managed .gitignore block keeps private contract folders out of git',
|
|
207
207
|
owner: 'meta/contracts.md',
|
|
208
|
-
// .gitignore plus what git currently TRACKS and IGNORES, which the tree
|
|
209
|
-
// carries and no read set records (the header says why)
|
|
210
|
-
inputs: ['.'],
|
|
211
208
|
// it untracks generated files (a commit) unless the run is frozen
|
|
212
209
|
writes: true,
|
|
210
|
+
runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
|
|
211
|
+
// .gitignore plus what git currently TRACKS and IGNORES, which the tree
|
|
212
|
+
// carries and no read set records (the header says why)
|
|
213
|
+
cache: { by: 'files', inputs: ['.'], key: null },
|
|
214
|
+
fix: 'ci',
|
|
215
|
+
applies: 'always',
|
|
213
216
|
run: async (c) => {
|
|
214
217
|
// a generated path still in the index is the half-applied state the
|
|
215
218
|
// managed block cannot fix on its own
|
|
@@ -278,7 +281,7 @@ export const gitignorePolicy = check({
|
|
|
278
281
|
}
|
|
279
282
|
c.ok(`.gitignore policy block current, ${claimed.length + backProbes.size} patterns verified against git`)
|
|
280
283
|
},
|
|
281
|
-
|
|
284
|
+
repair: async (c): Promise<FixResult> => {
|
|
282
285
|
// the move first: it writes the new block itself, before any folder moves
|
|
283
286
|
const moved = process.env.SHIPKIT_FROZEN === '1' ? null : migrateLayout(c.root, { config: c.config, renderGitignore: (t, legacy, cfg) => renderPolicy(t, cfg ?? c.config, legacy) })
|
|
284
287
|
const layout = moved == null ? null
|
|
@@ -10,7 +10,12 @@
|
|
|
10
10
|
// Per rvlib-* dep:
|
|
11
11
|
// link: → the symlink must resolve (a broken link dies cryptically at
|
|
12
12
|
// import time otherwise) — nothing to update, code is live
|
|
13
|
-
// semver → installed vs npm latest
|
|
13
|
+
// semver → installed vs npm latest. A semver pin whose node_modules
|
|
14
|
+
// entry is a `bun link` symlink is judged by the PIN's floor
|
|
15
|
+
// instead: the link carries the source, always the newest, while
|
|
16
|
+
// a fresh clone installs the pin. Behind, the pin moves through
|
|
17
|
+
// the same guarded update, and `bun link <name>` restores the
|
|
18
|
+
// link after it (a link that does not come back FAILs). A BREAKING jump (a new major, or a
|
|
14
19
|
// new 0.x minor) is never taken by itself
|
|
15
20
|
// (src/core/update-policy.ts: WARN naming `fleet --update --major`,
|
|
16
21
|
// package.json untouched); the rvlib-shipkit tree running this
|
|
@@ -46,7 +51,7 @@
|
|
|
46
51
|
import { existsSync, readFileSync } from 'fs'
|
|
47
52
|
import { basename, join } from 'path'
|
|
48
53
|
import type { CheckCtx } from '../core/check-ctx.ts'
|
|
49
|
-
import { allowMajor, breakingAdvice, isBreaking } from '../core/update-policy.ts'
|
|
54
|
+
import { allowMajor, breakingAdvice, isBreaking, isLinkedInstall, pinFloor } from '../core/update-policy.ts'
|
|
50
55
|
import { isRunningCopy, rememberLatest } from '../core/self-update.ts'
|
|
51
56
|
import { rootPublishesNpm } from '../core/repo-facts.ts'
|
|
52
57
|
import { mutateLease, withMutateLease } from '../core/locked-write.ts'
|
|
@@ -61,6 +66,13 @@ const installedVersion = (root: string, name: string): string | null => {
|
|
|
61
66
|
try { return (JSON.parse(readFileSync(p, 'utf8')) as Pkg).version ?? null } catch { return null }
|
|
62
67
|
}
|
|
63
68
|
|
|
69
|
+
const readPin = (root: string, name: string): string | null => {
|
|
70
|
+
try {
|
|
71
|
+
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as Pkg
|
|
72
|
+
return pkg.dependencies?.[name] ?? pkg.devDependencies?.[name] ?? null
|
|
73
|
+
} catch { return null }
|
|
74
|
+
}
|
|
75
|
+
|
|
64
76
|
async function latestVersion(t: CheckCtx, name: string): Promise<string> {
|
|
65
77
|
const res = await t.fetch(`https://registry.npmjs.org/${name}/latest`)
|
|
66
78
|
if (!res.ok) throw new Error(`registry ${res.status}`)
|
|
@@ -74,23 +86,24 @@ export const rvlibVersions = check({
|
|
|
74
86
|
blocking: true,
|
|
75
87
|
title: 'rvlib versions',
|
|
76
88
|
description: 'the rvlib packages installed here are as new as the published ones',
|
|
77
|
-
owner: 'build/shipkit.md',
|
|
78
|
-
ttl: '24h',
|
|
89
|
+
owner: 'build/shipkit/consuming.md',
|
|
79
90
|
// it updates the rvlib deps unless the run is frozen
|
|
80
91
|
writes: true,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
try { pkg = JSON.parse(readFileSync(join(env.root, 'package.json'), 'utf8')) as Pkg } catch { return 'no readable package.json' }
|
|
84
|
-
return Object.keys({ ...pkg.dependencies, ...pkg.devDependencies }).some((n) => n.startsWith('rvlib-')) ? null : 'no rvlib-* dependency'
|
|
85
|
-
},
|
|
86
|
-
cacheKey: (env) => {
|
|
92
|
+
runs: { onPreCommit: false, onCi: false, onFull: true, onRelease: true, onHealth: true, onSave: false, onClock: { every: '24h', inactive: 'wait' } },
|
|
93
|
+
cache: { by: 'ttl', span: '24h', key: (env) => {
|
|
87
94
|
let pkg: Pkg
|
|
88
95
|
try { pkg = JSON.parse(readFileSync(join(env.root, 'package.json'), 'utf8')) as Pkg } catch { return 'unreadable' }
|
|
89
96
|
return Object.keys({ ...pkg.dependencies, ...pkg.devDependencies })
|
|
90
97
|
.filter((n) => n.startsWith('rvlib-'))
|
|
91
98
|
.sort()
|
|
92
|
-
.map((n) => `${n}@${installedVersion(env.root, n) ?? 'absent'}`)
|
|
99
|
+
.map((n) => `${n}@${installedVersion(env.root, n) ?? 'absent'}${isLinkedInstall(env.root, n) ? ` pin ${readPin(env.root, n)}` : ''}`)
|
|
93
100
|
.join(',') + (rootPublishesNpm(env.config) ? ' npm' : '')
|
|
101
|
+
} },
|
|
102
|
+
fix: 'none',
|
|
103
|
+
applies: (env) => {
|
|
104
|
+
let pkg: Pkg
|
|
105
|
+
try { pkg = JSON.parse(readFileSync(join(env.root, 'package.json'), 'utf8')) as Pkg } catch { return 'no readable package.json' }
|
|
106
|
+
return Object.keys({ ...pkg.dependencies, ...pkg.devDependencies }).some((n) => n.startsWith('rvlib-')) ? null : 'no rvlib-* dependency'
|
|
94
107
|
},
|
|
95
108
|
run: async (c) => {
|
|
96
109
|
let pkg: Pkg
|
|
@@ -100,39 +113,47 @@ export const rvlibVersions = check({
|
|
|
100
113
|
const pm = detectPackageManager(c.root)
|
|
101
114
|
const manifest = ['package.json', ...(isRefusal(pm) ? LOCK_FILES : pm.locks)]
|
|
102
115
|
|
|
103
|
-
const stale: Array<{ name: string; installed: string; latest: string }> = []
|
|
116
|
+
const stale: Array<{ name: string; installed: string; latest: string; linked: boolean }> = []
|
|
104
117
|
for (const [name, spec] of deps) {
|
|
105
118
|
if (spec.startsWith('link:')) {
|
|
106
119
|
if (installedVersion(c.root, name) == null) c.fail(`${name} is link: but the symlink does not resolve — re-run \`bun link ${name}\` (a broken link dies cryptically at import time)`)
|
|
107
120
|
else c.ok(`${name} (linked, live)`)
|
|
108
121
|
continue
|
|
109
122
|
}
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
123
|
+
const onDisk = installedVersion(c.root, name)
|
|
124
|
+
if (onDisk == null) { c.fail(`${name} declared (${spec}) but not installed — ${isRefusal(pm) ? pm.refused : `run: ${pm.install.join(' ')}`}`); continue }
|
|
125
|
+
const linked = isLinkedInstall(c.root, name)
|
|
126
|
+
const floor = linked ? pinFloor(spec) : null
|
|
127
|
+
if (linked && floor == null) { c.ok(`${name} (linked to its source, pin ${spec} names no version to judge)`); continue }
|
|
128
|
+
const installed = floor ?? onDisk
|
|
129
|
+
// how the lines below name it: a linked dep is its pin, never the source it links
|
|
130
|
+
const label = linked ? `${name} pin ${installed}` : `${name} ${installed}`
|
|
112
131
|
let latest: string
|
|
113
132
|
try { latest = await latestVersion(c, name) } catch (e) {
|
|
114
|
-
c.lines.push({ kind: 'skip', text: `${
|
|
133
|
+
c.lines.push({ kind: 'skip', text: `${label} — registry unreachable (${String(e).slice(0, 80)}), version drift unknown` })
|
|
115
134
|
continue
|
|
116
135
|
}
|
|
117
136
|
// real semver order, not string difference — a dist-tag pointing
|
|
118
137
|
// BEHIND the installed version (prerelease, unpublish window)
|
|
119
138
|
// must never trigger a silent downgrade
|
|
120
139
|
const cmp = Bun.semver.order(latest, installed)
|
|
121
|
-
if (cmp === 0) c.ok(`${
|
|
122
|
-
else if (cmp < 0) c.ok(`${
|
|
140
|
+
if (cmp === 0) c.ok(`${label} (latest${linked ? ', linked to its source' : ''})`)
|
|
141
|
+
else if (cmp < 0) c.ok(`${label} ahead of npm latest ${latest} (prerelease or dist-tag lag) — not touching`)
|
|
123
142
|
else if (isBreaking(installed, latest) && !allowMajor()) c.warn(breakingAdvice(name, latest, basename(c.root)))
|
|
124
|
-
|
|
143
|
+
// a linked toolkit moves only its pin: the source tree this process
|
|
144
|
+
// runs from is never written, so the swap below cannot happen
|
|
145
|
+
else if (name === 'rvlib-shipkit' && !linked && isRunningCopy(c.root)) {
|
|
125
146
|
// swapping this tree under the running process is how a run
|
|
126
147
|
// meets modules of two versions. The startup self-update owns
|
|
127
148
|
// it: handing it the answer makes the next run update and
|
|
128
149
|
// restart before any check loads
|
|
129
150
|
rememberLatest(latest)
|
|
130
151
|
c.warn(`${name} ${installed} → ${latest} is the toolkit running this check, so it is not swapped mid run: the next shipkit run updates it and restarts before any check`)
|
|
131
|
-
} else stale.push({ name, installed, latest })
|
|
152
|
+
} else stale.push({ name, installed, latest, linked })
|
|
132
153
|
}
|
|
133
154
|
if (stale.length === 0) return
|
|
134
155
|
|
|
135
|
-
const drift = stale.map((s) => `${s.name} ${s.installed} → ${s.latest}`).join(', ')
|
|
156
|
+
const drift = stale.map((s) => `${s.name}${s.linked ? ' pin' : ''} ${s.installed} → ${s.latest}`).join(', ')
|
|
136
157
|
const names = stale.map((s) => s.name)
|
|
137
158
|
if (isRefusal(pm)) {
|
|
138
159
|
c.warn(`newer rvlib live: ${drift}, not updating: ${pm.refused}`)
|
|
@@ -164,7 +185,11 @@ export const rvlibVersions = check({
|
|
|
164
185
|
const leased = await withMutateLease(mutateLease(c.root, 'fix'), { label: 'self-update', print: () => {}, escape: 'SHIPKIT_FROZEN=1' }, async () => {
|
|
165
186
|
const again = await c.exec(['git', 'status', '--porcelain', '--', ...manifest], { cwd: c.root })
|
|
166
187
|
if (again.code === 0 && again.out.trim() !== '') return null
|
|
167
|
-
|
|
188
|
+
const up = await c.exec(cmd, { cwd: c.root, timeoutMs: 120_000 })
|
|
189
|
+
// the update may swap a link for the tarball: put every link back,
|
|
190
|
+
// whatever the update did, inside the same lease
|
|
191
|
+
for (const s of stale) if (s.linked) await c.exec(['bun', 'link', s.name], { cwd: c.root, timeoutMs: 60_000 })
|
|
192
|
+
return up
|
|
168
193
|
})
|
|
169
194
|
if (!leased.ok) {
|
|
170
195
|
c.warn(`newer rvlib live: ${drift} — self-update deferred (${leased.warn}); re-run check once that run finishes`)
|
|
@@ -188,6 +213,13 @@ export const rvlibVersions = check({
|
|
|
188
213
|
return
|
|
189
214
|
}
|
|
190
215
|
for (const s of stale) {
|
|
216
|
+
if (s.linked) {
|
|
217
|
+
const now = pinFloor(readPin(c.root, s.name) ?? '')
|
|
218
|
+
if (!isLinkedInstall(c.root, s.name)) c.fail(`${s.name} pin ${s.installed} → ${now ?? 'unreadable'} but node_modules/${s.name} is no longer linked to its source — run: bun link ${s.name}`)
|
|
219
|
+
else if (now === s.latest) c.warn(`${s.name} pin ${s.installed} → ${now} self-updated, link to the source restored — commit the bump`)
|
|
220
|
+
else c.warn(`${s.name} updated but its pin reads ${now ?? 'unreadable'} (wanted ${s.latest}) — check package.json`)
|
|
221
|
+
continue
|
|
222
|
+
}
|
|
191
223
|
const now = installedVersion(c.root, s.name)
|
|
192
224
|
// WARN on purpose: the run MUTATED the repo and a follow-up
|
|
193
225
|
// (restart watchers, commit) is owed — an ok here would be a
|
|
@@ -40,6 +40,10 @@ export const npmPublishSafety = check({
|
|
|
40
40
|
owner: 'meta/rv-conventions/package-json.md',
|
|
41
41
|
// keyed on what it reads: the root package.json and every declared release
|
|
42
42
|
// unit's, at the dirs the config picks
|
|
43
|
+
runs: { onPreCommit: false, onCi: false, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
|
|
44
|
+
cache: { by: 'reads' },
|
|
45
|
+
fix: 'none',
|
|
46
|
+
applies: 'always',
|
|
43
47
|
run: (c) => {
|
|
44
48
|
const json = (rel: string): Pkg | null | 'unparseable' => {
|
|
45
49
|
try { return c.json<Pkg>(rel) } catch { return 'unparseable' }
|
|
@@ -27,8 +27,10 @@ export const githubVisibility = check({
|
|
|
27
27
|
title: 'github visibility',
|
|
28
28
|
description: 'the github visibility of this repo matches the one it declares',
|
|
29
29
|
owner: 'meta/contracts.md',
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
runs: { onPreCommit: false, onCi: false, onFull: false, onRelease: false, onHealth: true, onSave: 'moved', onClock: 'once' },
|
|
31
|
+
cache: { by: 'fact', key: (env) => declaredVisibility(env.config) },
|
|
32
|
+
fix: 'none',
|
|
33
|
+
applies: 'always',
|
|
32
34
|
run: async (c) => {
|
|
33
35
|
const declared = declaredVisibility(c.config)
|
|
34
36
|
const skip = (why: string): void => void c.lines.push({ kind: 'skip', text: `github visibility unverified — ${why} (declared: ${declared})` })
|
|
@@ -86,15 +86,17 @@ export const reviewerAgent = check({
|
|
|
86
86
|
title: 'reviewer definition',
|
|
87
87
|
description: 'the declared reviewer agent has its brief, its definition and read only tools',
|
|
88
88
|
owner: 'identity/how-we-work.md',
|
|
89
|
+
runs: { onPreCommit: { budgetMs: 20 }, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
|
|
89
90
|
// the answer is the config, the definition file and the brief. The brief
|
|
90
91
|
// is a CONFIG path that may sit outside the repo, so both files enter the
|
|
91
92
|
// key by content, wherever they live
|
|
92
|
-
inputs: [CONFIG_FILE],
|
|
93
|
-
cacheKey: (env) => {
|
|
93
|
+
cache: { by: 'files', inputs: [CONFIG_FILE], key: (env) => {
|
|
94
94
|
const r = env.config.reviewer
|
|
95
95
|
if (r == null) return 'no reviewer'
|
|
96
96
|
return `${r.name}|${(r.tools ?? REVIEWER_TOOLS).join(',')}|def=${fileStamp(join(env.root, reviewerAgentPath(r.name)))}|${r.brief}=${fileStamp(configPath(env.root, r.brief))}`
|
|
97
|
-
},
|
|
97
|
+
} },
|
|
98
|
+
fix: 'ci',
|
|
99
|
+
applies: 'always',
|
|
98
100
|
run: (c) => {
|
|
99
101
|
const r = c.config.reviewer
|
|
100
102
|
if (r == null) return c.ok('no reviewer agent declared (repo.config reviewer:) — skipped')
|
|
@@ -109,7 +111,7 @@ export const reviewerAgent = check({
|
|
|
109
111
|
// create-if-missing only: an existing definition may carry a repo's own
|
|
110
112
|
// tuned prompt, and silently rewriting that would cost more than the
|
|
111
113
|
// finding it repairs. Violations in a present file stay a loud FAIL.
|
|
112
|
-
|
|
114
|
+
repair: (c): FixResult => {
|
|
113
115
|
const r = c.config.reviewer
|
|
114
116
|
if (r == null) return { changed: false, summary: 'no reviewer declared — nothing to write' }
|
|
115
117
|
const rel = reviewerAgentPath(r.name)
|
|
@@ -81,9 +81,11 @@ export const commitAiTells = check({
|
|
|
81
81
|
title: 'commit ai tells',
|
|
82
82
|
description: 'the commit-msg hook that refuses AI attribution is armed, and no ref name tells',
|
|
83
83
|
owner: 'identity/how-we-commit.md',
|
|
84
|
+
runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
|
|
84
85
|
// git config and ref names, which no tree id describes, so they are the key
|
|
85
|
-
inputs: [CONFIG_FILE],
|
|
86
|
-
|
|
86
|
+
cache: { by: 'files', inputs: [CONFIG_FILE], key: historyKey },
|
|
87
|
+
fix: 'none',
|
|
88
|
+
applies: 'always',
|
|
87
89
|
run: async (c) => {
|
|
88
90
|
const nested = nestedIn(c.root)
|
|
89
91
|
if (nested != null) return void c.lines.push({ kind: 'skip', text: `commit gates not checked, nested in ${nested}: the parent repo owns them` })
|
|
@@ -40,6 +40,10 @@ export const undeclaredUnit = check({
|
|
|
40
40
|
owner: 'build/shipkit/units.md',
|
|
41
41
|
// keyed on what it reads: the listing of every package.json, each one it
|
|
42
42
|
// opens, and the declared units' package.json
|
|
43
|
+
runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
|
|
44
|
+
cache: { by: 'reads' },
|
|
45
|
+
fix: 'none',
|
|
46
|
+
applies: 'always',
|
|
43
47
|
run: (c) => {
|
|
44
48
|
const { units, errors, testOnly } = resolveUnits(c.root, c.config, checkUnitReader(c))
|
|
45
49
|
// a DECLARED unit that does not resolve is a declaration bug and
|
|
@@ -61,6 +61,10 @@ export const siblingUnitRanges = check({
|
|
|
61
61
|
owner: 'build/shipkit/built-in-checks.md',
|
|
62
62
|
// keyed on what it reads: every declared unit's package.json, at the dirs
|
|
63
63
|
// the config picks
|
|
64
|
+
runs: { onPreCommit: false, onCi: false, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
|
|
65
|
+
cache: { by: 'reads' },
|
|
66
|
+
fix: 'none',
|
|
67
|
+
applies: 'always',
|
|
64
68
|
run: (c) => {
|
|
65
69
|
const { units } = resolveUnits(c.root, c.config, checkUnitReader(c))
|
|
66
70
|
const version = new Map(units.map((u) => [u.name, u.version]))
|
|
@@ -30,6 +30,10 @@ export const configBarrelImport = check({
|
|
|
30
30
|
title: 'config import path',
|
|
31
31
|
description: 'repo.config.ts imports `defineRepo` from the narrow module, never the barrel',
|
|
32
32
|
owner: 'build/shipkit/config-keys.md',
|
|
33
|
+
runs: { onPreCommit: { budgetMs: 20 }, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
|
|
34
|
+
cache: { by: 'reads' },
|
|
35
|
+
fix: 'ci',
|
|
36
|
+
applies: 'always',
|
|
33
37
|
run: (c) => {
|
|
34
38
|
const src = c.read(CONFIG)
|
|
35
39
|
if (src == null) return void c.ok(`${CONFIG} absent — nothing to narrow`)
|
|
@@ -43,7 +47,7 @@ export const configBarrelImport = check({
|
|
|
43
47
|
}
|
|
44
48
|
c.fail(`${CONFIG} imports from the package barrel (~400ms of module load on every shipkit run, the pre-commit hook included)`)
|
|
45
49
|
},
|
|
46
|
-
|
|
50
|
+
repair: (c): FixResult => {
|
|
47
51
|
const src = c.read(CONFIG)
|
|
48
52
|
if (src == null) return { changed: false, summary: `${CONFIG} absent` }
|
|
49
53
|
if (!BARREL.test(src)) return { changed: false, summary: 'already narrow' }
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// SK013 — the harness half of ONE WAY IN (build/shipkit.md). Rule SK001 routes
|
|
1
|
+
// SK013 — the harness half of ONE WAY IN (build/shipkit/one-way-in.md). Rule SK001 routes
|
|
2
2
|
// the package.json scripts, and that alone is not enough: `bun test` is a bun
|
|
3
3
|
// BUILTIN, so no script can intercept it and the reflex stays open. This rule
|
|
4
4
|
// makes the repo carry the two things that CAN close it, and repairs them:
|
|
@@ -19,7 +19,7 @@ import { rootPublishesNpm } from '../core/repo-facts.ts'
|
|
|
19
19
|
import type { RepoConfig } from '../repo-config.ts'
|
|
20
20
|
import { check, type CheckCtx, type FixResult } from '../check.ts'
|
|
21
21
|
|
|
22
|
-
const OWNER = 'build/shipkit.md'
|
|
22
|
+
const OWNER = 'build/shipkit/one-way-in.md'
|
|
23
23
|
export const SETTINGS_PATH = '.claude/settings.json'
|
|
24
24
|
|
|
25
25
|
type Settings = {
|
|
@@ -124,6 +124,10 @@ export const oneWayInPolicy = check({
|
|
|
124
124
|
// keyed on what it reads: .claude/settings.json, package.json, and whether
|
|
125
125
|
// the declared guard script resolves. A guard at an absolute path lives
|
|
126
126
|
// outside the repo, so an answer that probed one is never stored
|
|
127
|
+
runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
|
|
128
|
+
cache: { by: 'reads' },
|
|
129
|
+
fix: 'ci',
|
|
130
|
+
applies: 'always',
|
|
127
131
|
run: async (c) => {
|
|
128
132
|
if (rootPublishesNpm(c.config)) return c.ok(PUBLIC_SKIP)
|
|
129
133
|
const d = c
|
|
@@ -171,7 +175,7 @@ export const oneWayInPolicy = check({
|
|
|
171
175
|
// MERGES, never rewrites: this file also carries the attribution silencer,
|
|
172
176
|
// a repo's own allow list and its own hooks, and clobbering those to fix a
|
|
173
177
|
// deny list would cost more than the finding it repairs
|
|
174
|
-
|
|
178
|
+
repair: async (c): Promise<FixResult> => {
|
|
175
179
|
if (rootPublishesNpm(c.config)) return { changed: false, summary: PUBLIC_SKIP }
|
|
176
180
|
const d = c
|
|
177
181
|
const want = denyRules(exemptSlots(c.config))
|
|
@@ -30,6 +30,10 @@ export const proseSte = check({
|
|
|
30
30
|
owner: OWNER,
|
|
31
31
|
// keyed on what it reads: each include glob's listing and every file in
|
|
32
32
|
// scope, so an edit outside the declared prose never reruns it
|
|
33
|
+
runs: { onPreCommit: false, onCi: false, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
|
|
34
|
+
cache: { by: 'reads' },
|
|
35
|
+
fix: 'none',
|
|
36
|
+
applies: 'always',
|
|
33
37
|
run: (c) => {
|
|
34
38
|
const cfg = c.config.prose
|
|
35
39
|
if (cfg == null || (cfg.include ?? []).length === 0) {
|
|
@@ -12,10 +12,12 @@ export const nestedRepo = check({
|
|
|
12
12
|
title: 'nested repo',
|
|
13
13
|
description: 'a config inside a larger work tree declares `nested: true`, and only there',
|
|
14
14
|
owner: 'build/shipkit/config-keys.md',
|
|
15
|
+
runs: { onPreCommit: { budgetMs: 20 }, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
|
|
15
16
|
// the declaration, and where the git work tree root sits, which no tree
|
|
16
17
|
// id describes, so its path is the rest of the key
|
|
17
|
-
inputs: [CONFIG_FILE],
|
|
18
|
-
|
|
18
|
+
cache: { by: 'files', inputs: [CONFIG_FILE], key: (env) => `nested=${env.config.nested === true}|top=${nestedIn(env.root) ?? 'root'}` },
|
|
19
|
+
fix: 'none',
|
|
20
|
+
applies: 'always',
|
|
19
21
|
run: (c) => {
|
|
20
22
|
const top = nestedIn(c.root)
|
|
21
23
|
const declared = c.config.nested === true
|
|
@@ -49,6 +49,10 @@ export const legacyCheckShapes = check({
|
|
|
49
49
|
blocking: true,
|
|
50
50
|
// keyed on what it reads: the listing of check files, their sources, and
|
|
51
51
|
// the config. A gitignored check file is no declaration of this repo
|
|
52
|
+
runs: { onPreCommit: false, onCi: false, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
|
|
53
|
+
cache: { by: 'reads' },
|
|
54
|
+
fix: 'manual',
|
|
55
|
+
applies: 'always',
|
|
52
56
|
run: (c) => {
|
|
53
57
|
const s = legacyShapes(c, c.config)
|
|
54
58
|
if (s.count === 0 && s.placeholders.length === 0) {
|
|
@@ -70,7 +74,7 @@ export const legacyCheckShapes = check({
|
|
|
70
74
|
const head = s.checkFiles.slice(0, 3).map((f) => f.rel).join(', ')
|
|
71
75
|
c.warn(`${s.checkFiles.length} legacy check script(s) read by the static reader: ${head}${s.checkFiles.length > 3 ? `, and ${s.checkFiles.length - 3} more` : ''}`)
|
|
72
76
|
const off = s.checkFiles.filter((f) => !f.read.gate).length
|
|
73
|
-
if (off > 0) c.warn(`${off} of them carry \`export const gate = false\`, which is
|
|
77
|
+
if (off > 0) c.warn(`${off} of them carry \`export const gate = false\`, which is every gate axis false and onHealth true`)
|
|
74
78
|
}
|
|
75
79
|
if (s.checkDefaults) c.warn(`repo.config.ts checkDefaults lets a file forget \`blocking\` — the value belongs in each file`)
|
|
76
80
|
if (s.tasks.length > 0) {
|
|
@@ -78,10 +82,10 @@ export const legacyCheckShapes = check({
|
|
|
78
82
|
c.warn(`${s.tasks.length} \`tasks: []\` entr(ies), ${data} of them pure data — each one is a *.check.ts file with \`build: true\``)
|
|
79
83
|
}
|
|
80
84
|
if (s.rules.length > 0) c.warn(`${s.rules.length} \`rules: []\` entr(ies): ${s.rules.join(', ')} — each one is a *.check.ts file whose \`run\` is its \`lint\``)
|
|
81
|
-
if (s.ruleExempt.length > 0) c.warn(`ruleExempt names ${s.ruleExempt.join(', ')} — the spelling is \`
|
|
85
|
+
if (s.ruleExempt.length > 0) c.warn(`ruleExempt names ${s.ruleExempt.join(', ')} — the spelling is \`off: { id: '<why>' }\`, and \`shipkit ci\` ports it (SK029)`)
|
|
82
86
|
// what the fixer rewrites without guessing: the exemptions, the tasks
|
|
83
87
|
// once all of them are data, checkDefaults once no script leans on it
|
|
84
|
-
const mechanical =
|
|
88
|
+
const mechanical = (s.tasks.every((x) => x.data) ? s.tasks.length : 0) + (s.checkDefaults && s.checkFiles.length === 0 ? 1 : 0)
|
|
85
89
|
c.warn(
|
|
86
90
|
`${s.count} declaration(s) in a shape the check file replaced: still read, and refused by the first shipkit release after \`shipkit fleet\` shows no repo below 0.40 and none still carrying a check script`,
|
|
87
91
|
mechanical > 0
|
|
@@ -89,7 +93,7 @@ export const legacyCheckShapes = check({
|
|
|
89
93
|
: `nothing left is mechanical, so the port is by hand: each line above becomes a *.check.ts file (${OWNER})`,
|
|
90
94
|
)
|
|
91
95
|
},
|
|
92
|
-
|
|
96
|
+
repair: async (c) => await fixLegacyShapes(c),
|
|
93
97
|
})
|
|
94
98
|
|
|
95
99
|
export default legacyCheckShapes
|
|
@@ -162,17 +166,14 @@ export async function fixLegacyShapes(c: CheckCtx, rewrite: ConfigRewriter = inO
|
|
|
162
166
|
for (const r of config.rules ?? [])
|
|
163
167
|
hand.push(`rule ${r.id}: move it into ${checkFileName(r.id)} beside repo.config.ts, \`lint\` becomes \`run\` and \`code\` goes (the file's own path is its closure)`)
|
|
164
168
|
|
|
165
|
-
// 4. the
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
const cfgPlan: ConfigPlan = { when: {}, drop: [], name: config.name }
|
|
170
|
-
for (const [id, why] of Object.entries(config.ruleExempt ?? {})) cfgPlan.when[id] = `off:${why}`
|
|
169
|
+
// 4. the config keys, last: a key is only dropped once every declaration
|
|
170
|
+
// that leaned on it has a home. It loads the config, so it runs in its
|
|
171
|
+
// own process. `ruleExempt` is SK029's: it ports it into `off`
|
|
172
|
+
const cfgPlan: ConfigPlan = { off: {}, drop: [], name: config.name }
|
|
171
173
|
if (s.checkDefaults && filesLeft === 0) cfgPlan.drop.push({ key: 'checkDefaults', why: 'every file now declares its own blocking' })
|
|
172
174
|
else if (s.checkDefaults) hand.push(`repo.config.ts \`checkDefaults\` stays until the ${filesLeft} file(s) above declare their own \`blocking\``)
|
|
173
175
|
if (tasksMove) cfgPlan.drop.push({ key: 'tasks', why: 'every task is a check file now' })
|
|
174
|
-
if (
|
|
175
|
-
if (Object.keys(cfgPlan.when).length > 0 || cfgPlan.drop.length > 0) {
|
|
176
|
+
if (cfgPlan.drop.length > 0) {
|
|
176
177
|
const out = await rewrite(cfgPlan)
|
|
177
178
|
did.push(...out.did)
|
|
178
179
|
hand.push(...out.hand)
|
|
@@ -12,7 +12,7 @@ import { check } from '../check.ts'
|
|
|
12
12
|
import type { CheckCtx } from '../core/check-ctx.ts'
|
|
13
13
|
import { checkFloorReader, resolveFloor } from '../core/floor-resolve.ts'
|
|
14
14
|
import { rootPublishesNpm } from '../core/repo-facts.ts'
|
|
15
|
-
import {
|
|
15
|
+
import { repoOff } from '../core/when.ts'
|
|
16
16
|
import { hubProblemMatcher } from '../problems/problem-line.ts'
|
|
17
17
|
import { indentAt, parseSpans, plain, removalSpan, type Node } from '../scaffold/jsonc-spans.ts'
|
|
18
18
|
|
|
@@ -24,12 +24,15 @@ type Arr = Extract<Node, { kind: 'array' }>
|
|
|
24
24
|
|
|
25
25
|
const prop = (o: Obj, key: string): Node | undefined => o.props.find((p) => p.key === key)?.value
|
|
26
26
|
|
|
27
|
+
/** the type checker binaries: tsc, and tsgo, its native port */
|
|
28
|
+
const TSC_BINS = new Set(['tsc', 'tsgo'])
|
|
29
|
+
|
|
27
30
|
/** the house type checker task and nothing that only looks like it: the exact
|
|
28
|
-
* label, a tsc binary, in watch mode */
|
|
31
|
+
* label, a tsc or tsgo binary, in watch mode */
|
|
29
32
|
function isTscWatcher(t: Record<string, unknown>): boolean {
|
|
30
33
|
const cmd = typeof t['command'] === 'string' ? t['command'] : ''
|
|
31
34
|
const args = Array.isArray(t['args']) ? t['args'] : []
|
|
32
|
-
return t['label'] === TSC_LABEL && basename(cmd)
|
|
35
|
+
return t['label'] === TSC_LABEL && TSC_BINS.has(basename(cmd)) && args.includes('--watch')
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
/** the hub task: `bun run hub`, or the house label */
|
|
@@ -90,9 +93,17 @@ export function repairTasks(text: string, o: RepairOpts): { text: string; change
|
|
|
90
93
|
if (/\/\/.*(master task|subtask)/i.test(text)) changes.push('a comment still describes a master task with subtasks: rewrite it by hand')
|
|
91
94
|
}
|
|
92
95
|
|
|
96
|
+
// a wrapper that started only the TSC task starts nothing once it goes
|
|
97
|
+
const emptied = !dropTsc ? [] : tasks.items.flatMap((n, i) => (n !== wrapper && n.kind === 'object' && isWrapper(record(n)) && depsOf(n).length === 0 ? [i] : []))
|
|
98
|
+
for (const i of emptied) {
|
|
99
|
+
const [start, end] = removalSpan(text, tasks, i)
|
|
100
|
+
edits.push({ start, end, body: '' })
|
|
101
|
+
changes.push(`removed the '${labelOf(tasks.items[i]!)}' wrapper, which started nothing else`)
|
|
102
|
+
}
|
|
103
|
+
|
|
93
104
|
if (dropTsc) {
|
|
94
105
|
for (const t of items) {
|
|
95
|
-
if (t === wrapper) continue
|
|
106
|
+
if (t === wrapper || emptied.some((i) => tasks.items[i] === t)) continue
|
|
96
107
|
const d = prop(t, 'dependsOn')
|
|
97
108
|
if (d?.kind === 'array') {
|
|
98
109
|
const idx = d.items.findIndex((x) => x.kind === 'scalar' && x.value === TSC_LABEL)
|
|
@@ -148,13 +159,13 @@ export function repairTasks(text: string, o: RepairOpts): { text: string; change
|
|
|
148
159
|
}
|
|
149
160
|
|
|
150
161
|
/** whether this repo's gate runs a typecheck: a floor slot or a task that
|
|
151
|
-
* resolves, and no `
|
|
162
|
+
* resolves, and no `off` entry turning it off */
|
|
152
163
|
export function typecheckOn(c: CheckCtx): boolean {
|
|
153
164
|
const config = c.config
|
|
154
165
|
let floor: ReturnType<typeof resolveFloor>
|
|
155
166
|
try { floor = resolveFloor(config, c.root, checkFloorReader(c)) } catch { return false }
|
|
156
167
|
const t = [...floor.tasks, ...(config.tasks ?? [])].find((x) => x.id === 'typecheck')
|
|
157
|
-
return t != null &&
|
|
168
|
+
return t != null && repoOff(config, t.id) == null
|
|
158
169
|
}
|
|
159
170
|
|
|
160
171
|
/** the tasks file this check may touch: none, or left alone because .vscode/
|
|
@@ -178,6 +189,10 @@ export const vscodeHubTask = check({
|
|
|
178
189
|
title: 'vscode hub task',
|
|
179
190
|
description: 'the vscode hub task carries the problem matcher and no extra tsc watcher runs',
|
|
180
191
|
owner: 'build/vscode-tasks.md',
|
|
192
|
+
runs: { onPreCommit: false, onCi: false, onFull: false, onRelease: false, onHealth: true, onSave: 'moved', onClock: false },
|
|
193
|
+
cache: { by: 'reads' },
|
|
194
|
+
fix: 'manual',
|
|
195
|
+
applies: 'always',
|
|
181
196
|
run: (c) => {
|
|
182
197
|
if (rootPublishesNpm(c.config)) return c.ok(PUBLIC_SKIP)
|
|
183
198
|
const own = ownTasks(c)
|
|
@@ -190,7 +205,7 @@ export const vscodeHubTask = check({
|
|
|
190
205
|
if (r.changes.length === 0) return c.ok(`${TASKS}: the hub task feeds the problems pane`)
|
|
191
206
|
for (const ch of r.changes) c.fail(`${TASKS}: ${ch.replace(/^removed/, 'should remove').replace(/^dropped/, 'should drop').replace(/^wrote/, 'should write').replace(/^folded/, 'should fold')}`)
|
|
192
207
|
},
|
|
193
|
-
|
|
208
|
+
repair: (c) => {
|
|
194
209
|
if (rootPublishesNpm(c.config)) return { changed: false, summary: PUBLIC_SKIP }
|
|
195
210
|
const own = ownTasks(c)
|
|
196
211
|
if (typeof own === 'string') return { changed: false, summary: `no ${TASKS} to repair` }
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// SK018 — every test file runs in a check (build/shipkit.md
|
|
1
|
+
// SK018 — every test file runs in a check (build/shipkit/test-runner.md).
|
|
2
2
|
// A `*.test.ts` that no check runs is a green nobody measured: the file says
|
|
3
3
|
// something is proved, and nothing proves it. Each file belongs to the release
|
|
4
4
|
// unit whose dir holds it (the root takes the rest), and the unit's own
|
|
@@ -12,7 +12,11 @@ export const testFilesRun = check({
|
|
|
12
12
|
blocking: true,
|
|
13
13
|
title: 'test files run',
|
|
14
14
|
description: 'every test file in the repo runs in a test check or is ignored on purpose',
|
|
15
|
-
owner: 'build/shipkit.md',
|
|
15
|
+
owner: 'build/shipkit/test-runner.md',
|
|
16
|
+
runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
|
|
17
|
+
cache: { by: 'reads' },
|
|
18
|
+
fix: 'none',
|
|
19
|
+
applies: 'always',
|
|
16
20
|
run: (c) => {
|
|
17
21
|
const files = c.list(TEST_GLOB).filter(isTestFile)
|
|
18
22
|
if (files.length === 0) return c.ok('no test file in this repo')
|