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.
Files changed (199) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/README.md +17 -17
  3. package/package.json +3 -2
  4. package/src/assets/gen/comfyts-gen.ts +12 -8
  5. package/src/assets/server/studio-server.ts +14 -2
  6. package/src/check.ts +73 -103
  7. package/src/checks/SK001-package-json-shape.check.ts +11 -6
  8. package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
  9. package/src/checks/SK003-claude-md-shape.check.ts +7 -3
  10. package/src/checks/SK004-gitignore-policy.check.ts +7 -4
  11. package/src/checks/SK005-rvlib-versions.check.ts +53 -21
  12. package/src/checks/SK006-npm-publish-safety.check.ts +4 -0
  13. package/src/checks/SK007-github-visibility.check.ts +4 -2
  14. package/src/checks/SK008-reviewer-agent.check.ts +6 -4
  15. package/src/checks/SK009-commit-ai-tells.check.ts +4 -2
  16. package/src/checks/SK010-undeclared-unit.check.ts +4 -0
  17. package/src/checks/SK011-sibling-unit-ranges.check.ts +4 -0
  18. package/src/checks/SK012-config-barrel-import.check.ts +5 -1
  19. package/src/checks/SK013-one-way-in-policy.check.ts +7 -3
  20. package/src/checks/SK014-prose-ste.check.ts +4 -0
  21. package/src/checks/SK015-nested-repo.check.ts +4 -2
  22. package/src/checks/SK016-legacy-check-shapes.check.ts +13 -12
  23. package/src/checks/SK017-vscode-hub-task.check.ts +22 -7
  24. package/src/checks/SK018-test-files-run.check.ts +6 -2
  25. package/src/checks/SK019-tests-run-in-units.check.ts +8 -4
  26. package/src/checks/SK020-self-spawning-check.check.ts +4 -0
  27. package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -4
  28. package/src/checks/SK024-identity-colors.check.ts +5 -3
  29. package/src/checks/SK025-release-drift.check.ts +4 -5
  30. package/src/checks/SK026-asks-autoclose.check.ts +4 -4
  31. package/src/checks/SK027-agent-wiki-links.check.ts +4 -1
  32. package/src/checks/SK028-readme.check.ts +5 -1
  33. package/src/checks/SK029-explicit-check-files.check.ts +95 -0
  34. package/src/checks/SK030-cache-audit.check.ts +34 -0
  35. package/src/checks/SK031-trace-audit.check.ts +39 -0
  36. package/src/checks/SK032-ignored-inputs.check.ts +79 -0
  37. package/src/cli/cmd_Asks.ts +5 -5
  38. package/src/cli/cmd_Assets.ts +17 -13
  39. package/src/cli/cmd_Bump.ts +12 -11
  40. package/src/cli/cmd_CacheAudit.ts +43 -0
  41. package/src/cli/cmd_Check.ts +8 -7
  42. package/src/cli/cmd_Checks.ts +7 -6
  43. package/src/cli/cmd_Ci.ts +12 -11
  44. package/src/cli/cmd_Commands.ts +16 -13
  45. package/src/cli/cmd_ConfigKeys.ts +7 -6
  46. package/src/cli/cmd_Contracts.ts +3 -3
  47. package/src/cli/cmd_CrossRepoActivity.ts +8 -6
  48. package/src/cli/cmd_Fleet.ts +9 -7
  49. package/src/cli/cmd_GlobalProcesses.ts +120 -7
  50. package/src/cli/cmd_Home.ts +26 -19
  51. package/src/cli/cmd_Hooks.ts +6 -5
  52. package/src/cli/cmd_Init.ts +5 -4
  53. package/src/cli/cmd_Keys.ts +20 -8
  54. package/src/cli/cmd_Mv.ts +7 -6
  55. package/src/cli/cmd_Needs.ts +4 -3
  56. package/src/cli/cmd_Overview.ts +5 -4
  57. package/src/cli/cmd_Pb.ts +2 -2
  58. package/src/cli/cmd_Planned.ts +6 -5
  59. package/src/cli/cmd_Prose.ts +5 -5
  60. package/src/cli/cmd_Reflect.ts +7 -7
  61. package/src/cli/cmd_Release.ts +6 -5
  62. package/src/cli/cmd_Releases.ts +5 -4
  63. package/src/cli/cmd_Serve.ts +6 -4
  64. package/src/cli/cmd_Smoke.ts +5 -4
  65. package/src/cli/cmd_Start.ts +7 -6
  66. package/src/cli/cmd_Status.ts +4 -3
  67. package/src/cli/cmd_TraceAudit.ts +62 -0
  68. package/src/cli/cmd_Tui.ts +3 -2
  69. package/src/cli/cmd_When.ts +71 -0
  70. package/src/cli/cmd_Wiki.ts +4 -4
  71. package/src/cli.ts +11 -2
  72. package/src/collections/checks.ts +2 -0
  73. package/src/collections/collection-types.ts +12 -4
  74. package/src/collections/providers.ts +5 -3
  75. package/src/core/app-icon.ts +154 -0
  76. package/src/core/audit-report.ts +21 -0
  77. package/src/core/cache-audit.ts +72 -0
  78. package/src/core/canonical-scripts.ts +2 -2
  79. package/src/core/check-ctx.ts +14 -5
  80. package/src/core/check-runner.ts +43 -23
  81. package/src/core/config-rewrite.ts +37 -24
  82. package/src/core/discover-checks.ts +23 -2
  83. package/src/core/floor-resolve.ts +5 -7
  84. package/src/core/git-root.ts +13 -0
  85. package/src/core/ignored-block.ts +94 -0
  86. package/src/core/ignored-inputs.ts +47 -0
  87. package/src/core/jobs.ts +9 -2
  88. package/src/core/legacy-shapes.ts +39 -10
  89. package/src/core/one-lib.ts +59 -4
  90. package/src/core/one-way-in-policy.ts +2 -2
  91. package/src/core/outcome-words.ts +18 -2
  92. package/src/core/port-check-cli.ts +52 -0
  93. package/src/core/port-check-file.ts +295 -0
  94. package/src/core/port-check-files.ts +31 -0
  95. package/src/core/read-set.ts +97 -9
  96. package/src/core/retention.ts +0 -14
  97. package/src/core/run-check.ts +12 -9
  98. package/src/core/run-ci.ts +14 -5
  99. package/src/core/run-fix.ts +30 -23
  100. package/src/core/run-fixers.ts +48 -5
  101. package/src/core/runs-matrix.ts +179 -0
  102. package/src/core/runs.ts +242 -0
  103. package/src/core/trace-audit.ts +120 -0
  104. package/src/core/update-policy.ts +20 -0
  105. package/src/core/when-block.ts +29 -30
  106. package/src/core/when.ts +41 -103
  107. package/src/core/wire.ts +10 -14
  108. package/src/fleet/scan-fleet.ts +20 -4
  109. package/src/fleet/update-fleet.ts +6 -6
  110. package/src/home/gui-app.ts +24 -27
  111. package/src/home/home-launchd.ts +20 -4
  112. package/src/home/home-routes.ts +12 -1
  113. package/src/home/home-start.ts +11 -5
  114. package/src/home/hubs.ts +5 -5
  115. package/src/home/open-app.ts +2 -2
  116. package/src/home/pm.ts +8 -0
  117. package/src/home/run-pm.ts +1 -0
  118. package/src/home/serve-web.ts +2 -2
  119. package/src/home/supervise.ts +24 -1
  120. package/src/home/via-hub.ts +14 -0
  121. package/src/hooks/run-hooks.ts +48 -4
  122. package/src/init-repo.ts +6 -6
  123. package/src/ledger/db.ts +2 -13
  124. package/src/ledger/gate-checks.ts +133 -61
  125. package/src/ledger/git-key.ts +153 -31
  126. package/src/ledger/git-scan.ts +5 -2
  127. package/src/ledger/ledger.ts +136 -21
  128. package/src/ledger/store.ts +14 -0
  129. package/src/ledger/toolchain-identity.ts +248 -45
  130. package/src/plan/repo-source.ts +18 -39
  131. package/src/problems/extract.ts +3 -0
  132. package/src/release/run-release.ts +5 -0
  133. package/src/repo-config.ts +46 -54
  134. package/src/rules/rule.ts +2 -2
  135. package/src/scaffold/files/agent-coding.md +1 -1
  136. package/src/scaffold/files/agent-index.md +1 -1
  137. package/src/scaffold/files/verify-skill.md +2 -2
  138. package/src/serve/hub-app/activity-st.ts +5 -4
  139. package/src/serve/hub-app/activity-view.tsx +1 -1
  140. package/src/serve/hub-app/admin-svc.ts +36 -3
  141. package/src/serve/hub-app/admin-view.tsx +46 -12
  142. package/src/serve/hub-app/check-view.tsx +66 -46
  143. package/src/serve/hub-app/check-words.ts +1 -11
  144. package/src/serve/hub-app/checks-svc.ts +14 -35
  145. package/src/serve/hub-app/col-card.tsx +1 -44
  146. package/src/serve/hub-app/col-tables-view.tsx +84 -11
  147. package/src/serve/hub-app/col-tables.ts +27 -0
  148. package/src/serve/hub-app/cols-svc.ts +5 -4
  149. package/src/serve/hub-app/dock-st.ts +3 -2
  150. package/src/serve/hub-app/dock-view.tsx +36 -0
  151. package/src/serve/hub-app/dock.css +6 -0
  152. package/src/serve/hub-app/float-cards.tsx +30 -0
  153. package/src/serve/hub-app/floating.tsx +41 -4
  154. package/src/serve/hub-app/git-st.ts +42 -0
  155. package/src/serve/hub-app/git-view.tsx +61 -0
  156. package/src/serve/hub-app/hash-route.ts +45 -1
  157. package/src/serve/hub-app/header-bar.tsx +62 -72
  158. package/src/serve/hub-app/health-svc.ts +3 -2
  159. package/src/serve/hub-app/home-st.ts +14 -2
  160. package/src/serve/hub-app/home-strip.css +43 -12
  161. package/src/serve/hub-app/home-strip.tsx +115 -64
  162. package/src/serve/hub-app/home.html +1 -0
  163. package/src/serve/hub-app/hub-app.tsx +32 -8
  164. package/src/serve/hub-app/hub-types.ts +5 -2
  165. package/src/serve/hub-app/hub.css +89 -63
  166. package/src/serve/hub-app/icons.tsx +11 -0
  167. package/src/serve/hub-app/index.html +1 -0
  168. package/src/serve/hub-app/keys.ts +15 -4
  169. package/src/serve/hub-app/live-svc.ts +3 -2
  170. package/src/serve/hub-app/machine-st.ts +18 -22
  171. package/src/serve/hub-app/plan-view.tsx +51 -0
  172. package/src/serve/hub-app/planned-st.ts +9 -4
  173. package/src/serve/hub-app/processes-view.tsx +45 -0
  174. package/src/serve/hub-app/project-list.css +4 -1
  175. package/src/serve/hub-app/project-list.tsx +6 -3
  176. package/src/serve/hub-app/projects-svc.ts +25 -10
  177. package/src/serve/hub-app/prose-hits-st.ts +3 -2
  178. package/src/serve/hub-app/reach.ts +36 -0
  179. package/src/serve/hub-app/readme-svc.ts +3 -2
  180. package/src/serve/hub-app/repo-sections.ts +131 -0
  181. package/src/serve/hub-app/repo-st.ts +70 -14
  182. package/src/serve/hub-app/sidebar.tsx +17 -42
  183. package/src/serve/hub-app/timeline-svc.ts +3 -2
  184. package/src/serve/hub-app/tree-build.ts +151 -155
  185. package/src/serve/hub-app/tree-row.tsx +55 -12
  186. package/src/serve/hub-app/tree-svc.ts +97 -56
  187. package/src/serve/hub-app/when-view.tsx +89 -0
  188. package/src/serve/hub-app/when.css +17 -0
  189. package/src/serve/hub-runtime.ts +7 -18
  190. package/src/serve/project-gutter-route.ts +22 -1
  191. package/src/serve/run-serve.ts +1 -1
  192. package/src/serve/serve-hub.ts +4 -14
  193. package/src/suite/judge-suites.ts +6 -0
  194. package/src/trace/interpose.c +655 -0
  195. package/src/trace/read-trace.ts +485 -0
  196. package/src/trace/tracer.ts +282 -0
  197. package/src/serve/hub-app/processes-svc.ts +0 -63
  198. package/src/serve/hub-app/repo-sections.tsx +0 -61
  199. package/src/serve/repo-processes.ts +0 -89
@@ -1,5 +1,5 @@
1
- // SK019 — tests run through the generated test checks only (build/shipkit.md,
2
- // the test checks). The floor `test` slot and one `test:<unit>` per declared
1
+ // SK019 — tests run through the generated test checks only (build/shipkit/test-runner.md).
2
+ // The floor `test` slot and one `test:<unit>` per declared
3
3
  // unit are the ONE way tests run: in the unit's dir with its bunfig and
4
4
  // preload, parallel, timed against the budget, and planned file by file by
5
5
  // SK018. A hand written check that runs `bun test` itself is a second runner
@@ -9,7 +9,7 @@
9
9
  // Its own id rather than a line of SK018: SK018 answers "does every test file
10
10
  // run", this one "does anything else run them", and each is read from
11
11
  // different files (test files and bunfigs there, check sources and the config
12
- // here), so each keeps its own key, row and `when` switch.
12
+ // here), so each keeps its own key, row and `off` switch.
13
13
  import { dirname, posix, relative } from 'path'
14
14
  import { check } from '../check.ts'
15
15
  import { repoCheckSources } from '../core/legacy-shapes.ts'
@@ -36,7 +36,11 @@ export const testsRunInUnits = check({
36
36
  blocking: true,
37
37
  title: 'tests run in unit checks',
38
38
  description: 'no check runs the test runner itself: tests run only in the generated test checks',
39
- owner: 'build/shipkit.md',
39
+ owner: 'build/shipkit/test-runner.md',
40
+ runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
41
+ cache: { by: 'reads' },
42
+ fix: 'none',
43
+ applies: 'always',
40
44
  run: (c) => {
41
45
  // the toolkit's own checks (this file's dir) live inside the repo that
42
46
  // hosts it, and a built-in that WRITES a test script (SK001) is not a
@@ -132,6 +132,10 @@ export const selfSpawningCheck = check({
132
132
  description: 'no check file runs its own body by spawning itself',
133
133
  owner: OWNER,
134
134
  blocking: true,
135
+ runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
136
+ cache: { by: 'reads' },
137
+ fix: 'none',
138
+ applies: 'always',
135
139
  run: (c) => {
136
140
  const found = repoCheckSources(c).flatMap((f) => {
137
141
  if (f.src == null || isLegacyCheckSource(f.src)) return []
@@ -1,4 +1,4 @@
1
- // SK022 machine runs the pin (build/shipkit.md): on a machine with a house
1
+ // SK022 machine runs the pin (build/shipkit/home.md): on a machine with a house
2
2
  // every shipkit runs the house copy, whatever a repo installed, so a repo's
3
3
  // rvlib-shipkit pin is a minimum. It warns when the repo asks for a version
4
4
  // newer than the one this process runs: a rule or an api that pin relies on
@@ -24,11 +24,13 @@ export const machineRunsThePin = check({
24
24
  blocking: false,
25
25
  title: 'machine runs the pin',
26
26
  description: 'the shipkit this machine runs is at least the version this repo pins',
27
- owner: 'build/shipkit.md',
27
+ owner: 'build/shipkit/home.md',
28
+ runs: { onPreCommit: false, onCi: false, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
28
29
  // the pin, the install (gitignored) and the version this machine runs,
29
30
  // which is no file of the repo
30
- inputs: ['package.json'],
31
- cacheKey: (env) => `${LIB_VERSION}|installed=${fileStamp(join(env.root, 'node_modules', NAME, 'package.json'))}`,
31
+ cache: { by: 'files', inputs: ['package.json'], key: (env) => `${LIB_VERSION}|installed=${fileStamp(join(env.root, 'node_modules', NAME, 'package.json'))}` },
32
+ fix: 'none',
33
+ applies: 'always',
32
34
  run: (c) => {
33
35
  const pkg = c.json<Pkg>('package.json')
34
36
  const spec = pkg?.devDependencies?.[NAME] ?? pkg?.dependencies?.[NAME]
@@ -18,9 +18,11 @@ export const identityColors = check({
18
18
  title: 'identity colors',
19
19
  description: 'repo.config.ts declares identity colours: colorLight and colorDark, two hex',
20
20
  owner: 'build/shipkit/built-in-checks.md',
21
+ runs: { onPreCommit: false, onCi: false, onFull: false, onRelease: false, onHealth: true, onSave: 'moved', onClock: false },
21
22
  // the config, and the installed lib that says whether it knows the key
22
- inputs: [CONFIG_FILE],
23
- cacheKey: (env) => `installed=${fileStamp(join(env.root, INSTALLED_CONFIG_SRC))}`,
23
+ cache: { by: 'files', inputs: [CONFIG_FILE], key: (env) => `installed=${fileStamp(join(env.root, INSTALLED_CONFIG_SRC))}` },
24
+ fix: 'manual',
25
+ applies: 'always',
24
26
  run: (c) => {
25
27
  const text = c.read(CONFIG_FILE)
26
28
  if (text == null) return c.note(`no ${CONFIG_FILE}`)
@@ -32,7 +34,7 @@ export const identityColors = check({
32
34
  if (why != null) return c.warn(`${CONFIG_FILE}: ${why}`, FIX)
33
35
  c.ok(`identity ${id!.colorLight} on light, ${id!.colorDark} on dark`)
34
36
  },
35
- fix: async (c) => {
37
+ repair: async (c) => {
36
38
  const text = c.read(CONFIG_FILE)
37
39
  if (text == null) return { changed: false, summary: `no ${CONFIG_FILE}` }
38
40
  const have = identityOfText(text)
@@ -41,11 +41,10 @@ export const releaseDrift = check({
41
41
  title: 'release drift',
42
42
  description: 'every unit this repo releases matches npm and its push remote',
43
43
  owner: 'build/shipkit/built-in-checks.md',
44
- when: 'health',
45
- ttl: '20h',
46
- every: (last) => (last?.status === 'pass' ? ['20h', '28h'] : '4h'),
47
- planOff: (env) => (releasedUnits(env.root, env.config).length === 0 ? 'this repo releases nothing' : null),
48
- cacheKey: (env) => releasedUnits(env.root, env.config).map((u) => `${u.id}@${u.version}`).join(','),
44
+ runs: { onPreCommit: false, onCi: false, onFull: false, onRelease: false, onHealth: true, onSave: false, onClock: { every: (last) => (last?.status === 'pass' ? ['20h', '28h'] : '4h'), inactive: 'wait' } },
45
+ cache: { by: 'ttl', span: '20h', key: (env) => releasedUnits(env.root, env.config).map((u) => `${u.id}@${u.version}`).join(',') },
46
+ fix: 'none',
47
+ applies: (env) => (releasedUnits(env.root, env.config).length === 0 ? 'this repo releases nothing' : null),
49
48
  run: async (c) => {
50
49
  const ids = new Set(releasedUnits(c.root, c.config).map((u) => u.id))
51
50
  if (ids.size === 0) return void c.lines.push({ kind: 'skip', text: 'this repo releases nothing: no npm root, no release config, no units' })
@@ -14,11 +14,11 @@ export const asksAutoclose = check({
14
14
  title: 'asks auto close',
15
15
  description: 'no open ask whose own probe passes stays in the queue',
16
16
  owner: 'build/shipkit/built-in-checks.md',
17
- when: 'health',
18
- never: "runs each open ask's own probe, a shell command that reads the world",
19
- every: ['20h', '28h'],
17
+ runs: { onPreCommit: false, onCi: false, onFull: false, onRelease: false, onHealth: true, onSave: false, onClock: { every: ['20h', '28h'], inactive: 'wait' } },
18
+ cache: { by: 'never', why: "runs each open ask's own probe, a shell command that reads the world" },
19
+ fix: 'none',
20
20
  // it only probes an ask that carries one, and archives one marked done
21
- planOff: (env) => {
21
+ applies: (env) => {
22
22
  const dir = join(env.root, env.config.asks?.dir ?? LAYOUT.asks)
23
23
  if (!existsSync(dir)) return 'no asks queue'
24
24
  return listAsks(dir).some((a) => a.check != null || a.status === 'done') ? null : 'no open ask carries a probe'
@@ -14,8 +14,11 @@ export const agentWikiLinks = check({
14
14
  title: 'agent wiki links',
15
15
  description: 'every agent/ page is linked from somewhere, and every agent/ link resolves',
16
16
  owner: 'meta/agent-folder.md',
17
+ runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
17
18
  // the md lint walks agent/ on disk rather than through `c`: agent/ is all it reads
18
- inputs: ['agent'],
19
+ cache: { by: 'files', inputs: ['agent'], key: null },
20
+ fix: 'none',
21
+ applies: 'always',
19
22
  run: (c) => {
20
23
  if (!existsSync(join(c.root, 'agent/index.md'))) return void c.lines.push({ kind: 'skip', text: 'no agent/ wiki in this repo' })
21
24
  const lint = lintMd({ root: join(c.root, 'agent'), index: 'index.md', boundary: c.root })
@@ -60,7 +60,7 @@ const REQUIREMENTS: Requirement[] = [
60
60
 
61
61
  /** the other way out, for a repo that cannot show one: the rule off, with the
62
62
  * reason `defineRepo` requires (core/when.ts) */
63
- const OFF = `or, when this repo cannot show one, turn the rule off with the reason: when: { SK028: 'off:<why this repo shows no screenshot>' } in repo.config.ts, inside its shipkit gate block`
63
+ const OFF = `or, when this repo cannot show one, turn the rule off with the reason: off: { SK028: '<why this repo shows no screenshot>' } in repo.config.ts, inside its shipkit gate block`
64
64
 
65
65
  export const readme = check({
66
66
  id: 'SK028',
@@ -68,6 +68,10 @@ export const readme = check({
68
68
  title: 'readme',
69
69
  description: 'README.md exists and shows at least one screenshot',
70
70
  owner: 'build/shipkit/built-in-checks.md',
71
+ runs: { onPreCommit: false, onCi: false, onFull: false, onRelease: false, onHealth: true, onSave: 'moved', onClock: false },
72
+ cache: { by: 'reads' },
73
+ fix: 'none',
74
+ applies: 'always',
71
75
  run: (c) => {
72
76
  const md = c.read(README)
73
77
  if (md == null) return c.warn(`${README}: exists: no ${README} at the repo root`, `write README.md: what the repo is, and a screenshot of it; ${OFF}`)
@@ -0,0 +1,95 @@
1
+ // SK029 explicit check files (build/shipkit/runs.md owns the declaration):
2
+ // every check file of the repo declares runs, cache, fix and applies, with
3
+ // no retired key. A file that does not does not load, and discovery shows
4
+ // it as one failing row. The repair ports it with the values the retired
5
+ // keys resolved to (core/port-check-file.ts), so the check keeps running
6
+ // where it ran, and the ci fix pass applies it before the gate reads the
7
+ // files: a repo carried across by its first `shipkit ci`. The same holds for
8
+ // the config: a retired `when` (only its `off:<why>` entries still load) and
9
+ // `ruleExempt` become `off` entries in the managed block.
10
+ import { join } from 'path'
11
+ import { check, type FixResult } from '../check.ts'
12
+ import { unportedCheckFiles } from '../core/port-check-files.ts'
13
+ import type { PortRun } from '../core/port-check-cli.ts'
14
+ import type { ConfigPlan, ConfigRewrite } from '../core/config-rewrite.ts'
15
+ import type { RepoConfig } from '../repo-config.ts'
16
+
17
+ /** the write half, run as its own process: it walks import closures on disk */
18
+ const PORT_CLI = join(import.meta.dir, '../core/port-check-cli.ts')
19
+ /** the config half, run as its own process: it loads repo.config.ts */
20
+ const CONFIG_REWRITE = join(import.meta.dir, '../core/config-rewrite.ts')
21
+
22
+ /** the retired config keys this repo still carries, and the `off` entries
23
+ * they become. defineRepo already refused every `when` word but `off:<why>` */
24
+ export function retiredOff(c: RepoConfig): { keys: string[]; off: Record<string, string> } {
25
+ const off: Record<string, string> = {}
26
+ for (const [id, raw] of Object.entries(c.when ?? {})) off[id] = raw.trim().replace(/^off:?/, '').trim()
27
+ for (const [id, why] of Object.entries(c.ruleExempt ?? {})) off[id] ??= why
28
+ const keys = [...(c.when != null ? ['when'] : []), ...(c.ruleExempt != null ? ['ruleExempt'] : [])]
29
+ return { keys, off }
30
+ }
31
+
32
+ export const explicitCheckFiles = check({
33
+ id: 'SK029',
34
+ title: 'explicit check files',
35
+ description: 'every check file declares runs, cache, fix and applies, and the config says only off',
36
+ owner: 'build/shipkit/runs.md',
37
+ blocking: true,
38
+ runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
39
+ // the listing of check files and their sources, read through `c`
40
+ cache: { by: 'reads' },
41
+ // the port is mechanical: every value it writes is what the retired keys
42
+ // resolved to, so the fix pass applies it without asking
43
+ fix: 'ci',
44
+ applies: 'always',
45
+ run: (c) => {
46
+ const files = unportedCheckFiles(c)
47
+ const cfg = retiredOff(c.config)
48
+ if (files.length === 0 && cfg.keys.length === 0) return c.ok('every check file declares runs, cache, fix and applies, and the config says only off')
49
+ for (const f of files) c.fail(`${f.rel} still declares the retired shape (${f.keys.join(', ')}), so it does not load`)
50
+ if (cfg.keys.length > 0) c.fail(`repo.config.ts still carries ${cfg.keys.join(' and ')}: a repo only turns a check off, as off: { <id>: '<why>' }`)
51
+ c.fail(`${files.length + (cfg.keys.length > 0 ? 1 : 0)} file(s) to port`, 'shipkit check --fix SK029 ports each one with the values it ran on, and `shipkit ci` does it in its fix pass')
52
+ },
53
+ repair: async (c): Promise<FixResult> => {
54
+ const files = unportedCheckFiles(c)
55
+ const cfg = retiredOff(c.config)
56
+ if (files.length === 0 && cfg.keys.length === 0) return { changed: false, summary: 'every check file is explicit already, and the config says only off' }
57
+ const did: string[] = []
58
+ const hand: string[] = []
59
+ if (files.length > 0) {
60
+ const r = await c.exec([process.execPath, PORT_CLI, c.root, ...files.map((f) => f.rel)], { cwd: c.root, timeoutMs: 60_000 })
61
+ try {
62
+ const run = JSON.parse(r.out.trim().split('\n').at(-1) ?? '') as PortRun
63
+ if (run.did.length > 0) did.push(`ported ${run.did.join('; ')}`)
64
+ hand.push(...run.hand)
65
+ } catch {
66
+ hand.push(`the port did not answer (exit ${r.code}: ${r.out.trim().slice(0, 160)}): port ${files.map((f) => f.rel).join(', ')} by hand (build/shipkit/runs.md)`)
67
+ }
68
+ }
69
+ if (cfg.keys.length > 0) {
70
+ // `when` sits inside the managed block, so it goes first and the
71
+ // block describes the config again before the entries land
72
+ const plan: ConfigPlan = {
73
+ off: cfg.off,
74
+ drop: [
75
+ ...(c.config.when != null ? [{ key: 'when', why: 'its off entries are `off` now', phase: 'first' as const }] : []),
76
+ ...(c.config.ruleExempt != null ? [{ key: 'ruleExempt', why: 'its exemptions are `off` now', phase: 'afterOff' as const }] : []),
77
+ ],
78
+ name: c.config.name,
79
+ }
80
+ const r = await c.exec([process.execPath, CONFIG_REWRITE, c.root, JSON.stringify(plan)], { cwd: c.root, timeoutMs: 60_000 })
81
+ try {
82
+ const out = JSON.parse(r.out.trim().split('\n').at(-1) ?? '') as ConfigRewrite
83
+ did.push(...out.did)
84
+ hand.push(...out.hand)
85
+ if (out.restored != null) hand.push(`repo.config.ts was left as it was: ${out.restored}`)
86
+ } catch {
87
+ hand.push(`the repo.config.ts rewrite did not answer (exit ${r.code}): write off: ${JSON.stringify(cfg.off)} by hand and drop ${cfg.keys.join(', ')}`)
88
+ }
89
+ }
90
+ const summary = [did.length > 0 ? did.join('; ') : 'nothing ported', ...(hand.length > 0 ? [`${hand.length} by hand: ${hand.join(' | ')}`] : [])].join('. ')
91
+ return { changed: did.length > 0, summary }
92
+ },
93
+ })
94
+
95
+ export default explicitCheckFiles
@@ -0,0 +1,34 @@
1
+ // SK030 cache audit (build/shipkit/runs.md owns the cache): a sample of the
2
+ // checks the cache answers is re-run fresh about once a day, and a cached
3
+ // green a fresh run disagrees with fails, naming the check whose key misses a
4
+ // path. The work is `sk cache-audit` (core/cache-audit.ts), in its own
5
+ // process, because it runs other checks.
6
+ import { join } from 'path'
7
+ import { check } from '../check.ts'
8
+
9
+ const CLI = join(import.meta.dir, '../cli.ts')
10
+
11
+ export const cacheAudit = check({
12
+ id: 'SK030',
13
+ title: 'cache audit',
14
+ description: 'a sample of cached answers matches what a fresh run of each check says',
15
+ owner: 'build/shipkit/runs.md',
16
+ blocking: true,
17
+ // never a gate: it re-runs other checks. The daemon asks it about once a
18
+ // day, and only while somebody works in the repo
19
+ runs: { onPreCommit: false, onCi: false, onFull: false, onRelease: false, onHealth: true, onSave: false, onClock: { every: ['20h', '28h'], inactive: 'wait' } },
20
+ cache: { by: 'never', why: 'it runs other checks fresh, so its answer is that run itself' },
21
+ fix: 'none',
22
+ applies: 'always',
23
+ run: async (c) => {
24
+ const r = await c.exec([process.execPath, CLI, 'cache-audit', '--json'], { cwd: c.root, timeoutMs: 600_000 })
25
+ let out: { pool: number; rows: { id: string; cached: string; fresh: string; keyedOn: string }[]; stale: { id: string; cached: string; fresh: string; keyedOn: string }[] }
26
+ try { out = JSON.parse(r.out.trim().split('\n').at(-1) ?? '') } catch { return c.fail(`sk cache-audit did not answer (exit ${r.code}: ${r.out.trim().slice(0, 160)})`) }
27
+ if (out.rows.length === 0) return c.note(`nothing to audit: the cache answers none of this repo's checks right now`)
28
+ for (const s of out.stale)
29
+ c.fail(`${s.id}: the cache said ${s.cached}, a fresh run says ${s.fresh}, so its key misses something it reads`, `${s.keyedOn === 'files' ? 'add the path to its cache.inputs' : 'move the read through c'}, then sk keys --why ${s.id}`)
30
+ if (out.stale.length === 0) c.ok(`${out.rows.map((x) => x.id).join(', ')}: every cached answer is what a fresh run says (${out.rows.length} of ${out.pool} cached)`)
31
+ },
32
+ })
33
+
34
+ export default cacheAudit
@@ -0,0 +1,39 @@
1
+ // SK031 traced audit (build/shipkit/cache.md owns the key): about once a day
2
+ // each gate task runs under the tracer, and a gitignored path it reads that
3
+ // its key misses joins that task's key from the kept answer. SK032 carries the
4
+ // kept proposals into repo.config.ts in the gate's fix pass. The work is
5
+ // `sk trace-audit` (core/trace-audit.ts), in its own process, because it runs
6
+ // the gate's tasks. On wherever the tracer runs, unless the machine opted out.
7
+ import { join } from 'path'
8
+ import { check } from '../check.ts'
9
+ import { traceUnavailable } from '../trace/tracer.ts'
10
+
11
+ const CLI = join(import.meta.dir, '../cli.ts')
12
+
13
+ export const traceAuditCheck = check({
14
+ id: 'SK031',
15
+ title: 'traced audit',
16
+ description: 'every gitignored path a gate task reads is in its key',
17
+ owner: 'build/shipkit/cache.md',
18
+ blocking: false,
19
+ // never a gate: it runs the gate's tasks. The daemon asks it about once a
20
+ // day, and only while somebody works in the repo
21
+ runs: { onPreCommit: false, onCi: false, onFull: false, onRelease: false, onHealth: true, onSave: false, onClock: { every: ['20h', '28h'], inactive: 'wait' } },
22
+ cache: { by: 'never', why: 'it runs the gate tasks under the tracer, so its answer is that run itself' },
23
+ fix: 'none',
24
+ applies: () => traceUnavailable(),
25
+ run: async (c) => {
26
+ const r = await c.exec([process.execPath, CLI, 'trace-audit', '--json'], { cwd: c.root, timeoutMs: 1_200_000 })
27
+ let out: { skipped?: string; tasks: { id: string; status: string; why?: string; uncovered: string[]; propose: string[] }[] }
28
+ try { out = JSON.parse(r.out.trim().split('\n').at(-1) ?? '') } catch { return c.fail(`sk trace-audit did not answer (exit ${r.code}: ${r.out.trim().slice(0, 160)})`) }
29
+ if (out.skipped != null) return c.note(`nothing traced: ${out.skipped}`)
30
+ for (const t of out.tasks.filter((x) => x.status === 'unaudited')) c.warn(`${t.id} could not be audited: ${t.why}`)
31
+ const missing = out.tasks.filter((x) => x.uncovered.length > 0)
32
+ // a finding joins its task's key at once (core/audit-report.ts), so it
33
+ // is news, never a failure: SK032 carries it into repo.config.ts
34
+ for (const t of missing) c.ok(`${t.id} reads ${t.uncovered.length} gitignored path(s) the config does not name, keyed from this audit: ${t.propose.join(', ')}`)
35
+ if (missing.length === 0) c.ok(`${out.tasks.filter((x) => x.status === 'audited').length} gate task(s) audited: every gitignored path they read is in their key`)
36
+ },
37
+ })
38
+
39
+ export default traceAuditCheck
@@ -0,0 +1,79 @@
1
+ // SK032 ignored inputs current (build/shipkit/cache.md owns the key): what the
2
+ // last traced audit (SK031) found a task reading outside its key is in
3
+ // repo.config.ts ignoredInputs, so other machines and clones key it too. The repair writes the missing entries into
4
+ // the managed block, so the gate's fix pass carries each finding into the
5
+ // config. Nothing is traced here: it reads the audit SK031 kept.
6
+ import { join } from 'path'
7
+ import { check } from '../check.ts'
8
+ import { describeByTask, pendingByTask, type AuditReport } from '../core/ignored-inputs.ts'
9
+
10
+ const CLI = join(import.meta.dir, '../cli.ts')
11
+
12
+ /** the first rvlib-shipkit release whose `defineRepo` knows `ignoredInputs`.
13
+ * A repo whose pin allows an older one loads its config elsewhere (a CI job
14
+ * installing from npm, a fresh clone) with that older copy, which refuses
15
+ * the unknown key: so nothing is written until the pin says at least this */
16
+ export const IGNORED_INPUTS_SINCE = '1.6.0'
17
+
18
+ const parts = (v: string): number[] => v.split('.').map((n) => Number.parseInt(n, 10) || 0)
19
+ const atLeast = (v: string, min: string): boolean => {
20
+ const a = parts(v), b = parts(min)
21
+ for (let i = 0; i < 3; i++) if ((a[i] ?? 0) !== (b[i] ?? 0)) return (a[i] ?? 0) > (b[i] ?? 0)
22
+ return true
23
+ }
24
+
25
+ /** why the repo's pin cannot carry the key yet, or null. No pin (the repo
26
+ * that hosts the toolkit, or one running it from source) can carry it */
27
+ function pinTooOld(pkg: string | null): string | null {
28
+ if (pkg == null) return null
29
+ let range: string | undefined
30
+ try {
31
+ const j = JSON.parse(pkg) as { dependencies?: Record<string, string>; devDependencies?: Record<string, string> }
32
+ range = j.devDependencies?.['rvlib-shipkit'] ?? j.dependencies?.['rvlib-shipkit']
33
+ } catch {
34
+ return null
35
+ }
36
+ const min = range == null ? null : /(\d+\.\d+\.\d+)/.exec(range)?.[1] ?? null
37
+ if (min == null || range!.startsWith('link:') || range!.startsWith('file:') || range!.startsWith('workspace:')) return null
38
+ return atLeast(min, IGNORED_INPUTS_SINCE) ? null : `package.json pins rvlib-shipkit ${range}, which allows a release before ${IGNORED_INPUTS_SINCE} that refuses ignoredInputs: raise the pin to ^${IGNORED_INPUTS_SINCE} first`
39
+ }
40
+
41
+ /** the kept audit, read through c so the reads key follows it */
42
+ function kept(text: string | null): AuditReport | null {
43
+ try { return text == null ? null : (JSON.parse(text) as AuditReport) } catch { return null }
44
+ }
45
+
46
+ export const ignoredInputsCheck = check({
47
+ id: 'SK032',
48
+ title: 'ignored inputs current',
49
+ description: 'every gitignored path the traced audit saw a task read is in ignoredInputs',
50
+ owner: 'build/shipkit/cache.md',
51
+ blocking: false,
52
+ runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: false, onHealth: true, onSave: false, onClock: false },
53
+ cache: { by: 'reads' },
54
+ fix: 'ci',
55
+ applies: 'always',
56
+ run: (c) => {
57
+ const missing = pendingByTask(kept(c.state('trace-audit.json')), c.config.ignoredInputs ?? {})
58
+ const n = Object.values(missing).flat().length
59
+ if (n === 0) return c.ok('repo.config.ts ignoredInputs covers what the last traced audit found')
60
+ // the keys on this machine already read the kept audit (core/audit-report.ts),
61
+ // so what is missing here is the copy other machines and clones see
62
+ const old = pinTooOld(c.read('package.json'))
63
+ if (old != null) return c.ok(`${n} path(s) the traced audit found are keyed on this machine only: ${old}`)
64
+ c.warn(`the traced audit found ${describeByTask(missing)}: keyed on this machine, not yet in repo.config.ts ignoredInputs`, 'sk ci writes them, and sk trace-audit --fix says why when it cannot')
65
+ },
66
+ repair: async (c) => {
67
+ const missing = pendingByTask(kept(c.state('trace-audit.json')), c.config.ignoredInputs ?? {})
68
+ if (Object.keys(missing).length === 0) return { changed: false, summary: 'nothing to add' }
69
+ const old = pinTooOld(c.read('package.json'))
70
+ if (old != null) return { changed: false, summary: old }
71
+ // the managed block writer loads repo.config.ts, which no read through
72
+ // c can stand for, so it runs in its own process
73
+ const r = await c.exec([process.execPath, CLI, 'trace-audit', '--fix'], { cwd: c.root, timeoutMs: 60_000 })
74
+ if (r.code !== 0) return { changed: false, summary: `sk trace-audit --fix failed: ${r.out.trim().split('\n').at(-1) ?? `exit ${r.code}`}` }
75
+ return { changed: true, summary: `repo.config.ts ignoredInputs gained ${describeByTask(missing)}` }
76
+ },
77
+ })
78
+
79
+ export default ignoredInputsCheck
@@ -15,7 +15,7 @@ export class AsksCommand extends Command {
15
15
  static override usage = Command.Usage({
16
16
  category: 'Asks',
17
17
  description: 'list open asks — the per-repo human action queue',
18
- examples: [['list open asks', 'shipkit asks']],
18
+ examples: [['list open asks', 'sk asks']],
19
19
  })
20
20
 
21
21
  async execute(): Promise<number> {
@@ -37,7 +37,7 @@ export class AsksCheckCommand extends Command {
37
37
  static override usage = Command.Usage({
38
38
  category: 'Asks',
39
39
  description: 'probe every open ask and archive the ones whose check now passes',
40
- examples: [['probe + auto-close', 'shipkit asks check']],
40
+ examples: [['probe + auto-close', 'sk asks check']],
41
41
  })
42
42
 
43
43
  async execute(): Promise<number> {
@@ -59,9 +59,9 @@ export class AskNewCommand extends Command {
59
59
  category: 'Asks',
60
60
  description: 'create a new ask file in the queue',
61
61
  examples: [
62
- ['minimal', 'shipkit ask "rotate the API token"'],
63
- ['with a reason', 'shipkit ask "buy the domain" --why "landing page is blocked on it"'],
64
- ['with metadata', 'shipkit ask "buy the domain" --kind action --blocks deploy --check "dig +short example.com"'],
62
+ ['minimal', 'sk ask "rotate the API token"'],
63
+ ['with a reason', 'sk ask "buy the domain" --why "landing page is blocked on it"'],
64
+ ['with metadata', 'sk ask "buy the domain" --kind action --blocks deploy --check "dig +short example.com"'],
65
65
  ],
66
66
  })
67
67
 
@@ -2,7 +2,11 @@
2
2
  // (ComfyUI or procedural canvas), curate in the studio, ship with metadata
3
3
  // write-through. Thin clipanion surfaces; logic in assets/run-assets.ts.
4
4
  import { Command, Option } from 'clipanion'
5
- import { runAssetsCheck, runAssetsGen, runAssetsIdentity, runAssetsTopUp, type AssetsIo } from '../assets/run-assets.ts'
5
+ import type { AssetsIo } from '../assets/run-assets.ts'
6
+
7
+ // loaded when an assets command RUNS: it pulls in canvas, sharp and comfy-ts,
8
+ // which every other command would otherwise pay for at startup
9
+ const assets = (): Promise<typeof import('../assets/run-assets.ts')> => import('../assets/run-assets.ts')
6
10
 
7
11
  const ioOf = (cmd: Command): AssetsIo => ({
8
12
  print: (l) => cmd.context.stdout.write(`${l}\n`),
@@ -19,8 +23,8 @@ export class AssetsFillCommand extends Command {
19
23
  category: 'Assets',
20
24
  description: 'top rolls up to N per asset (canvas or comfy), sparsest-first',
21
25
  examples: [
22
- ['every asset up to 8 rolls', 'shipkit assets fill 8'],
23
- ['only two categories', 'shipkit assets fill 8 icons sprites'],
26
+ ['every asset up to 8 rolls', 'sk assets fill 8'],
27
+ ['only two categories', 'sk assets fill 8 icons sprites'],
24
28
  ],
25
29
  })
26
30
 
@@ -33,7 +37,7 @@ export class AssetsFillCommand extends Command {
33
37
  this.context.stderr.write(`FAIL: '${this.n}' is not a positive roll count — usage: shipkit assets fill <N> [category…]\n`)
34
38
  return 1
35
39
  }
36
- return runAssetsTopUp(process.cwd(), ioOf(this), { mode: 'fill', n, categories: this.categories })
40
+ return (await assets()).runAssetsTopUp(process.cwd(), ioOf(this), { mode: 'fill', n, categories: this.categories })
37
41
  }
38
42
  }
39
43
 
@@ -43,7 +47,7 @@ export class AssetsAddCommand extends Command {
43
47
  static override usage = Command.Usage({
44
48
  category: 'Assets',
45
49
  description: 'N more rolls per asset (canvas or comfy), regardless of what it has',
46
- examples: [['3 fresh rolls everywhere', 'shipkit assets add 3']],
50
+ examples: [['3 fresh rolls everywhere', 'sk assets add 3']],
47
51
  })
48
52
 
49
53
  n = Option.String({ required: true })
@@ -55,7 +59,7 @@ export class AssetsAddCommand extends Command {
55
59
  this.context.stderr.write(`FAIL: '${this.n}' is not a positive roll count — usage: shipkit assets add <N> [category…]\n`)
56
60
  return 1
57
61
  }
58
- return runAssetsTopUp(process.cwd(), ioOf(this), { mode: 'add', n, categories: this.categories })
62
+ return (await assets()).runAssetsTopUp(process.cwd(), ioOf(this), { mode: 'add', n, categories: this.categories })
59
63
  }
60
64
  }
61
65
 
@@ -66,8 +70,8 @@ export class AssetsGenCommand extends Command {
66
70
  category: 'Assets',
67
71
  description: 'one-shot rolls for a single asset (canvas or comfy)',
68
72
  examples: [
69
- ['one roll', 'shipkit assets gen hero'],
70
- ['five rolls', 'shipkit assets gen hero --n 5'],
73
+ ['one roll', 'sk assets gen hero'],
74
+ ['five rolls', 'sk assets gen hero --n 5'],
71
75
  ],
72
76
  })
73
77
 
@@ -76,7 +80,7 @@ export class AssetsGenCommand extends Command {
76
80
 
77
81
  async execute(): Promise<number> {
78
82
  const n = Math.max(1, Number(this.n) || 1)
79
- return runAssetsGen(process.cwd(), ioOf(this), { assetId: this.assetId, n })
83
+ return (await assets()).runAssetsGen(process.cwd(), ioOf(this), { assetId: this.assetId, n })
80
84
  }
81
85
  }
82
86
 
@@ -86,11 +90,11 @@ export class AssetsIdentityCommand extends Command {
86
90
  static override usage = Command.Usage({
87
91
  category: 'Assets',
88
92
  description: 'render favicon/logo/OG from .shipkit/identity/brand.ts into public/',
89
- examples: [['regenerate the identity outputs', 'shipkit assets identity']],
93
+ examples: [['regenerate the identity outputs', 'sk assets identity']],
90
94
  })
91
95
 
92
96
  async execute(): Promise<number> {
93
- return runAssetsIdentity(process.cwd(), ioOf(this))
97
+ return (await assets()).runAssetsIdentity(process.cwd(), ioOf(this))
94
98
  }
95
99
  }
96
100
 
@@ -100,10 +104,10 @@ export class AssetsCheckCommand extends Command {
100
104
  static override usage = Command.Usage({
101
105
  category: 'Assets',
102
106
  description: 'kit loads, kinds valid, cflow workflows load, drafts present, identity outputs present',
103
- examples: [['health-check the asset pipeline', 'shipkit assets check']],
107
+ examples: [['health-check the asset pipeline', 'sk assets check']],
104
108
  })
105
109
 
106
110
  async execute(): Promise<number> {
107
- return runAssetsCheck(process.cwd(), ioOf(this))
111
+ return (await assets()).runAssetsCheck(process.cwd(), ioOf(this))
108
112
  }
109
113
  }
@@ -9,18 +9,19 @@ export class BumpCommand extends Command {
9
9
 
10
10
  static override usage = Command.Usage({
11
11
  category: 'Gate',
12
- description: 'ship a version in one word: bumps a unit (default the repo root), run from a feature branch it merges into the release branch first, then promotes the CHANGELOG Unreleased section, bumps package.json, runs the gate and every prepublish script, commits, tags, pushes, and cuts the release. Any failure before the version commit rolls the merge back and returns you to your branch.',
12
+ description: 'ship a version: merge, changelog, bump, gate, tag, push, release',
13
+ details: 'ship a version in one word: bumps a unit (default the repo root), run from a feature branch it merges into the release branch first, then promotes the CHANGELOG Unreleased section, bumps package.json, runs the gate and every prepublish script, commits, tags, pushes, and cuts the release. Any failure before the version commit rolls the merge back and returns you to your branch.',
13
14
  examples: [
14
- ['a bugfix release of the repo itself', 'shipkit bump patch'],
15
- ['a feature release', 'shipkit bump minor'],
16
- ['a breaking release', 'shipkit bump major'],
17
- ['one sub-unit (ids: shipkit releases)', 'shipkit bump minor rvlib-shipkit'],
18
- ['a whole mixed batch in ONE gate, each unit at the kind its heading declares', 'shipkit bump --all'],
19
- ['every unit carrying Unreleased notes, all at the same kind', 'shipkit bump patch --all'],
20
- ['one unit, kind from its own heading', 'shipkit bump rvlib-shipkit'],
21
- ['rehearse the whole thing', 'shipkit bump minor --dry-run'],
22
- ['bump and commit, release later', 'shipkit bump patch --no-release'],
23
- ['from a feature branch, refuse rather than merge', 'shipkit bump patch --no-merge'],
15
+ ['a bugfix release of the repo itself', 'sk bump patch'],
16
+ ['a feature release', 'sk bump minor'],
17
+ ['a breaking release', 'sk bump major'],
18
+ ['one sub-unit (ids: shipkit releases)', 'sk bump minor rvlib-shipkit'],
19
+ ['a whole mixed batch in ONE gate, each unit at the kind its heading declares', 'sk bump --all'],
20
+ ['every unit carrying Unreleased notes, all at the same kind', 'sk bump patch --all'],
21
+ ['one unit, kind from its own heading', 'sk bump rvlib-shipkit'],
22
+ ['rehearse the whole thing', 'sk bump minor --dry-run'],
23
+ ['bump and commit, release later', 'sk bump patch --no-release'],
24
+ ['from a feature branch, refuse rather than merge', 'sk bump patch --no-merge'],
24
25
  ],
25
26
  })
26
27
 
@@ -0,0 +1,43 @@
1
+ // sk cache-audit — re-runs a sample of the checks the cache would answer right
2
+ // now, fresh, and compares: a cached green a fresh run disagrees with is a
3
+ // key that misses something its check reads (core/cache-audit.ts).
4
+ import { Command, Option } from 'clipanion'
5
+ import { loadRepoConfig } from '../repo-config.ts'
6
+ import { auditCache } from '../core/cache-audit.ts'
7
+
8
+ export class CacheAuditCommand extends Command {
9
+ static override paths = [['cache-audit']]
10
+
11
+ static override usage = Command.Usage({
12
+ category: 'Health',
13
+ description: 'is a cached green still true: re-run a sample of cached checks fresh and compare',
14
+ details: 'Picks checks the cache would answer right now (keyed on files or on reads), a different stable sample each day, runs each one fresh and compares its verdict with the cached one. A mismatch means the check key misses a path its body reads: the fresh answer replaces the stale one, and the line names the check. Exit 1 on a mismatch.',
15
+ examples: [
16
+ ['audit three cached checks', 'sk cache-audit'],
17
+ ['a bigger sample', 'sk cache-audit --sample 10'],
18
+ ['one check, by id', 'sk cache-audit --only SK004'],
19
+ ['machine-readable', 'sk cache-audit --json'],
20
+ ],
21
+ })
22
+
23
+ sample = Option.String('--sample', '3', { description: 'how many cached checks to re-run fresh (default 3)' })
24
+ only = Option.Array('--only', { description: 'audit these check ids instead of a sample' })
25
+ json = Option.Boolean('--json', false, { description: 'machine-readable { pool, rows, stale }' })
26
+
27
+ async execute(): Promise<number> {
28
+ const root = process.cwd()
29
+ const n = Number(this.sample)
30
+ if (!Number.isInteger(n) || n < 1) { this.context.stdout.write(`FAIL: --sample takes a whole number above 0, got '${this.sample}'\n`); return 1 }
31
+ const r = await auditCache(root, await loadRepoConfig(root), { sample: n, only: this.only })
32
+ if (this.json) { this.context.stdout.write(`${JSON.stringify(r)}\n`); return r.stale.length > 0 ? 1 : 0 }
33
+ if (r.rows.length === 0) { this.context.stdout.write(`skip: nothing to audit, the cache answers none of ${this.only?.join(', ') ?? 'the checks'} right now\n`); return 0 }
34
+ for (const row of r.rows) {
35
+ const same = row.fresh === row.cached || row.fresh === 'skip'
36
+ this.context.stdout.write(same
37
+ ? `ok: ${row.id} cached ${row.cached}, fresh ${row.fresh} (${row.ms}ms)\n`
38
+ : `FAIL: ${row.id} cached ${row.cached}, but a fresh run says ${row.fresh}: its key misses something it reads\n fix: ${row.keyedOn === 'files' ? `add the path its body reads to its cache.inputs` : 'move the read through c, or declare cache files or never'}, then sk keys --why ${row.id}\n`)
39
+ }
40
+ this.context.stdout.write(r.stale.length === 0 ? `ok: ${r.rows.length} of ${r.pool} cached check(s) audited, every cached answer is what a fresh run says\n` : `FAIL: ${r.stale.length} of ${r.rows.length} audited check(s) served a stale answer\n`)
41
+ return r.stale.length > 0 ? 1 : 0
42
+ }
43
+ }