rvlib-shipkit 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +105 -0
- package/README.md +17 -17
- package/package.json +3 -2
- package/src/asks/asks-queue.ts +4 -2
- package/src/assets/kits/identity-kit.ts +15 -13
- package/src/assets/run-assets.ts +12 -2
- package/src/check.ts +73 -98
- package/src/checks/SK001-package-json-shape.check.ts +26 -4
- package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
- package/src/checks/SK003-claude-md-shape.check.ts +5 -1
- package/src/checks/SK004-gitignore-policy.check.ts +7 -4
- package/src/checks/SK005-rvlib-versions.check.ts +53 -21
- package/src/checks/SK006-npm-publish-safety.check.ts +4 -0
- package/src/checks/SK007-github-visibility.check.ts +4 -2
- package/src/checks/SK008-reviewer-agent.check.ts +6 -4
- package/src/checks/SK009-commit-ai-tells.check.ts +94 -74
- package/src/checks/SK010-undeclared-unit.check.ts +4 -0
- package/src/checks/SK011-sibling-unit-ranges.check.ts +4 -0
- package/src/checks/SK012-config-barrel-import.check.ts +5 -1
- package/src/checks/SK013-one-way-in-policy.check.ts +7 -3
- package/src/checks/SK014-prose-ste.check.ts +20 -6
- package/src/checks/SK015-nested-repo.check.ts +4 -2
- package/src/checks/SK016-legacy-check-shapes.check.ts +23 -16
- package/src/checks/SK017-vscode-hub-task.check.ts +22 -7
- package/src/checks/SK018-test-files-run.check.ts +6 -2
- package/src/checks/SK019-tests-run-in-units.check.ts +8 -4
- package/src/checks/SK020-self-spawning-check.check.ts +4 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -4
- package/src/checks/SK024-identity-colors.check.ts +5 -3
- package/src/checks/SK025-release-drift.check.ts +4 -5
- package/src/checks/SK026-asks-autoclose.check.ts +4 -4
- package/src/checks/SK027-agent-wiki-links.check.ts +34 -9
- package/src/checks/SK028-readme.check.ts +10 -2
- package/src/checks/SK029-explicit-check-files.check.ts +95 -0
- package/src/cli/cmd_Asks.ts +5 -5
- package/src/cli/cmd_Assets.ts +7 -7
- package/src/cli/cmd_Bump.ts +12 -11
- package/src/cli/cmd_Check.ts +8 -7
- package/src/cli/cmd_Checks.ts +7 -6
- package/src/cli/cmd_Ci.ts +12 -11
- package/src/cli/cmd_Commands.ts +16 -13
- package/src/cli/cmd_ConfigKeys.ts +7 -6
- package/src/cli/cmd_Contracts.ts +3 -3
- package/src/cli/cmd_CrossRepoActivity.ts +10 -6
- package/src/cli/cmd_Fleet.ts +9 -7
- package/src/cli/cmd_GlobalProcesses.ts +116 -4
- package/src/cli/cmd_Home.ts +23 -17
- package/src/cli/cmd_Hooks.ts +6 -5
- package/src/cli/cmd_Init.ts +5 -4
- package/src/cli/cmd_Keys.ts +8 -7
- package/src/cli/cmd_Mv.ts +7 -6
- package/src/cli/cmd_Needs.ts +4 -3
- package/src/cli/cmd_Overview.ts +4 -3
- package/src/cli/cmd_Pb.ts +2 -2
- package/src/cli/cmd_Planned.ts +6 -5
- package/src/cli/cmd_Prose.ts +5 -5
- package/src/cli/cmd_Reflect.ts +7 -7
- package/src/cli/cmd_Release.ts +6 -5
- package/src/cli/cmd_Releases.ts +5 -4
- package/src/cli/cmd_Serve.ts +4 -3
- package/src/cli/cmd_Smoke.ts +5 -4
- package/src/cli/cmd_Start.ts +7 -6
- package/src/cli/cmd_Status.ts +4 -3
- package/src/cli/cmd_Tui.ts +3 -2
- package/src/cli/cmd_When.ts +71 -0
- package/src/cli/cmd_Wiki.ts +4 -4
- package/src/cli.ts +7 -2
- package/src/collections/checks.ts +2 -0
- package/src/collections/collection-types.ts +24 -4
- package/src/collections/providers.ts +6 -2
- package/src/contracts/apply-contracts.ts +9 -8
- package/src/contracts/contract-types.ts +8 -2
- package/src/contracts/contracts-engine.ts +34 -13
- package/src/contracts/manifests/identity.contract.ts +6 -5
- package/src/contracts/manifests/social.contract.ts +4 -2
- package/src/contracts/manifests/steam.contract.ts +7 -3
- package/src/contracts/web-surface.ts +59 -0
- package/src/core/budget.ts +19 -9
- package/src/core/canonical-scripts.ts +1 -1
- package/src/core/check-runner.ts +63 -25
- package/src/core/config-rewrite.ts +37 -24
- package/src/core/discover-checks.ts +31 -7
- package/src/core/layout.ts +2 -0
- package/src/core/legacy-shapes.ts +39 -10
- package/src/core/motherhome.ts +88 -66
- package/src/core/one-way-in-policy.ts +2 -2
- package/src/core/outcome-words.ts +18 -2
- package/src/core/port-check-cli.ts +52 -0
- package/src/core/port-check-file.ts +295 -0
- package/src/core/port-check-files.ts +31 -0
- package/src/core/retention.ts +0 -14
- package/src/core/run-check.ts +14 -14
- package/src/core/run-ci.ts +11 -7
- package/src/core/run-fix.ts +30 -23
- package/src/core/run-fixers.ts +42 -5
- package/src/core/runs-matrix.ts +179 -0
- package/src/core/runs.ts +242 -0
- package/src/core/update-policy.ts +20 -0
- package/src/core/when-block.ts +29 -30
- package/src/core/when.ts +41 -103
- package/src/core/wire.ts +34 -9
- package/src/fleet/scan-fleet.ts +20 -4
- package/src/fleet/update-fleet.ts +6 -6
- package/src/home/activity-shape.ts +4 -1
- package/src/home/activity.ts +22 -15
- package/src/home/gui-app/ShipkitGui.swift +144 -25
- package/src/home/gui-app.ts +93 -30
- package/src/home/home-launchd.ts +30 -6
- package/src/home/home-routes.ts +14 -2
- package/src/home/home-start.ts +33 -5
- package/src/home/hubs.ts +5 -5
- package/src/home/machine-stores.ts +4 -2
- package/src/home/open-app.ts +23 -10
- package/src/home/pm-config.ts +15 -11
- package/src/home/pm-inputs.ts +76 -0
- package/src/home/pm.ts +11 -3
- package/src/home/process-rows.ts +23 -6
- package/src/home/resources.ts +67 -15
- package/src/home/run-daemon.ts +6 -1
- package/src/home/run-pm.ts +30 -5
- package/src/home/serve-web.ts +2 -2
- package/src/home/supervise.ts +24 -1
- package/src/home/widgets.ts +32 -3
- package/src/hooks/commit-message.ts +2 -3
- package/src/hooks/run-hooks.ts +60 -6
- package/src/init-repo.ts +6 -6
- package/src/ledger/gate-checks.ts +115 -61
- package/src/ledger/git-key.ts +5 -1
- package/src/ledger/ledger.ts +5 -5
- package/src/ledger/store.ts +11 -0
- package/src/ledger/toolchain-identity.ts +45 -27
- package/src/ledger/world-cache.ts +17 -15
- package/src/plan/repo-source.ts +18 -39
- package/src/release/run-release.ts +5 -0
- package/src/repo-config.ts +36 -56
- package/src/rules/rule.ts +2 -2
- package/src/scaffold/files/agent-coding.md +1 -1
- package/src/scaffold/files/agent-index.md +1 -1
- package/src/scaffold/files/verify-skill.md +2 -2
- package/src/serve/hub-app/activity-st.ts +5 -4
- package/src/serve/hub-app/activity-view.tsx +4 -4
- package/src/serve/hub-app/admin-view.tsx +13 -11
- package/src/serve/hub-app/check-view.tsx +66 -46
- package/src/serve/hub-app/check-words.ts +1 -11
- package/src/serve/hub-app/checks-svc.ts +16 -36
- package/src/serve/hub-app/col-tables-view.tsx +131 -0
- package/src/serve/hub-app/col-tables.ts +115 -0
- package/src/serve/hub-app/cols-svc.ts +33 -4
- package/src/serve/hub-app/dock-st.ts +3 -2
- package/src/serve/hub-app/dock-view.tsx +36 -0
- package/src/serve/hub-app/dock.css +7 -1
- package/src/serve/hub-app/float-cards.tsx +30 -0
- package/src/serve/hub-app/floating.tsx +41 -4
- package/src/serve/hub-app/git-st.ts +42 -0
- package/src/serve/hub-app/git-view.tsx +61 -0
- package/src/serve/hub-app/hash-route.ts +45 -1
- package/src/serve/hub-app/header-bar.tsx +62 -72
- package/src/serve/hub-app/health-svc.ts +3 -2
- package/src/serve/hub-app/home-st.ts +15 -3
- package/src/serve/hub-app/home-strip.css +69 -19
- package/src/serve/hub-app/home-strip.tsx +192 -118
- package/src/serve/hub-app/home.html +1 -0
- package/src/serve/hub-app/hub-app.tsx +21 -5
- package/src/serve/hub-app/hub-types.ts +11 -5
- package/src/serve/hub-app/hub.css +88 -44
- package/src/serve/hub-app/icons.tsx +15 -0
- package/src/serve/hub-app/index.html +1 -0
- package/src/serve/hub-app/keys.ts +11 -8
- package/src/serve/hub-app/live-svc.ts +3 -2
- package/src/serve/hub-app/machine-st.ts +34 -49
- package/src/serve/hub-app/palette-st.ts +5 -5
- package/src/serve/hub-app/palette-view.tsx +1 -1
- package/src/serve/hub-app/plan-view.tsx +51 -0
- package/src/serve/hub-app/planned-st.ts +9 -4
- package/src/serve/hub-app/processes-view.tsx +38 -0
- package/src/serve/hub-app/project-list.tsx +3 -3
- package/src/serve/hub-app/projects-svc.ts +15 -10
- package/src/serve/hub-app/prose-hits-st.ts +3 -2
- package/src/serve/hub-app/reach.ts +36 -0
- package/src/serve/hub-app/readme-svc.ts +3 -2
- package/src/serve/hub-app/repo-sections.ts +117 -0
- package/src/serve/hub-app/repo-st.ts +47 -10
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/sidebar.tsx +12 -37
- package/src/serve/hub-app/timeline-svc.ts +3 -2
- package/src/serve/hub-app/timeline-view.tsx +14 -11
- package/src/serve/hub-app/tree-build.ts +149 -155
- package/src/serve/hub-app/tree-row.tsx +39 -9
- package/src/serve/hub-app/tree-svc.ts +70 -49
- package/src/serve/hub-app/when-view.tsx +89 -0
- package/src/serve/hub-app/when.css +17 -0
- package/src/serve/hub-runtime.ts +3 -16
- package/src/serve/project-gutter.ts +1 -2
- package/src/serve/run-serve.ts +1 -1
- package/src/serve/serve-hub.ts +4 -14
- package/src/serve/warm-cell.ts +49 -16
- package/src/suite/judge-suites.ts +42 -17
- package/src/suite/suite-cli.ts +5 -8
- package/src/wiki/lint-md.ts +2 -0
- package/src/wiki/scan-md.ts +37 -10
- package/src/serve/hub-app/processes-svc.ts +0 -63
- package/src/serve/hub-app/repo-sections.tsx +0 -61
- package/src/serve/repo-processes.ts +0 -89
|
@@ -1,23 +1,22 @@
|
|
|
1
|
-
// SK009
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// pushed history is the owner's decision, never an automatic rewrite; only
|
|
10
|
-
// an unpushed tip commit may be amended, and the fails say so.
|
|
1
|
+
// SK009 commit AI tells (identity/how-we-commit.md): the commit-msg hook is
|
|
2
|
+
// what refuses AI attribution, in every repo, so this check verifies the hook
|
|
3
|
+
// is ARMED: shipkit's own hooksPath, or a foreign one whose commit-msg chains
|
|
4
|
+
// the shipkit gate. It never scans history: what is pushed is accepted as it
|
|
5
|
+
// is, and only a new commit can still be stopped. A declared-public repo also
|
|
6
|
+
// needs the tracked attribution silencer, and its ref NAMES are checked (a
|
|
7
|
+
// FABLE-checkpoint tag outs the agent like a trailer); a branch nobody pushed
|
|
8
|
+
// is named "local only" and warns.
|
|
11
9
|
import { createHash } from 'crypto'
|
|
12
10
|
import { readFileSync } from 'fs'
|
|
13
|
-
import { join } from 'path'
|
|
14
|
-
import { hasAiAttribution, hasDashPunctuation } from '../hooks/commit-message.ts'
|
|
11
|
+
import { isAbsolute, join } from 'path'
|
|
15
12
|
import { nestedIn } from '../core/nesting.ts'
|
|
16
13
|
import { gitBin } from '../core/git-bin.ts'
|
|
17
14
|
import { fileStamp } from '../core/file-stamp.ts'
|
|
18
15
|
import { CONFIG_FILE } from '../core/check-pass.ts'
|
|
16
|
+
import { LAYOUT } from '../core/layout.ts'
|
|
19
17
|
import { check, type CheckEnv } from '../check.ts'
|
|
20
18
|
import { declaredVisibility } from '../core/repo-facts.ts'
|
|
19
|
+
import { hookBinInvocation } from '../hooks/run-hooks.ts'
|
|
21
20
|
|
|
22
21
|
// the documented silencer: .claude/settings.json attribution.commit/.pr both
|
|
23
22
|
// empty — the harness then never writes the trailer or the PR footer
|
|
@@ -30,81 +29,102 @@ const attributionSilenced = (root: string): boolean => {
|
|
|
30
29
|
}
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
const readOrNull = (abs: string): string | null => {
|
|
33
|
+
try {
|
|
34
|
+
return readFileSync(abs, 'utf8')
|
|
35
|
+
} catch {
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** a commit-msg body that runs the shipkit gate on the message: a live line
|
|
41
|
+
* (never a comment) whose failure is not swallowed by `|| true` */
|
|
42
|
+
const chainsShipkit = (body: string): boolean =>
|
|
43
|
+
body.split('\n').some((l) => !l.trim().startsWith('#') && /\bhooks\s+commit-msg\b/.test(l) && !/\|\|\s*(?:true|:)\b/.test(l))
|
|
44
|
+
|
|
45
|
+
/** the chain line to paste into a foreign hook: a clone with no install skips
|
|
46
|
+
* the gate with a note, rather than failing every commit */
|
|
47
|
+
export const guardedChain = (bin: string): string => {
|
|
48
|
+
const cli = bin.replace(/^bun\s+/, '')
|
|
49
|
+
return `if [ -f ${cli} ]; then ${bin} hooks commit-msg "$1" || exit 1; else echo "commit-msg: rvlib-shipkit not installed (run bun install), attribution gate skipped" >&2; fi`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ref NAMES tell too. bare match only for names with no other meaning;
|
|
53
|
+
// vendor and codename words (cursor, openai, gemini, opus, fable, …) name
|
|
54
|
+
// legitimate branches and features, they only match in a checkpoint/session shape
|
|
55
|
+
const REF_TELL = /\b(?:claude|chatgpt|copilot|aider|devin)\b|(?:^|[-_/])(?:fable|sonnet|haiku|opus|gemini|codex)[-_](?:checkpoint|session|ckpt)/i
|
|
56
|
+
|
|
57
|
+
const gitOut = (root: string, args: string[]): { code: number; out: string } => {
|
|
58
|
+
const p = Bun.spawnSync([gitBin(), ...args], { cwd: root, stdout: 'pipe', stderr: 'pipe' })
|
|
59
|
+
return { code: p.exitCode, out: p.stdout.toString() }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** what the answer depends on: the declared visibility, the hooksPath and the
|
|
63
|
+
* commit-msg file it points at, and for a public repo every ref NAME and the
|
|
64
|
+
* silencer. A commit moves none of them */
|
|
36
65
|
export function historyKey(env: CheckEnv): string {
|
|
37
|
-
const declared = declaredVisibility(env.config)
|
|
38
66
|
const nested = nestedIn(env.root)
|
|
39
67
|
if (nested != null) return `nested in ${nested}`
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
const refs =
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
return `public|refs=${digest}|settings=${fileStamp(join(env.root, '.claude', 'settings.json'))}`
|
|
68
|
+
const declared = declaredVisibility(env.config)
|
|
69
|
+
const hooksPath = gitOut(env.root, ['config', 'core.hooksPath'])
|
|
70
|
+
const dir = hooksPath.code === 0 ? hooksPath.out.trim() : ''
|
|
71
|
+
const hook = `hooks=${dir}|msg=${dir === '' ? 'none' : fileStamp(isAbsolute(dir) ? join(dir, 'commit-msg') : join(env.root, dir, 'commit-msg'))}`
|
|
72
|
+
if (declared !== 'public') return `declared ${declared}|${hook}`
|
|
73
|
+
const refs = gitOut(env.root, ['for-each-ref', '--format=%(refname)', 'refs/tags', 'refs/heads', 'refs/remotes']).out
|
|
74
|
+
const digest = createHash('sha256').update(refs).digest('hex').slice(0, 16)
|
|
75
|
+
return `public|${hook}|refs=${digest}|settings=${fileStamp(join(env.root, '.claude', 'settings.json'))}`
|
|
49
76
|
}
|
|
50
77
|
|
|
51
78
|
export const commitAiTells = check({
|
|
52
79
|
id: 'SK009',
|
|
53
80
|
blocking: true,
|
|
54
81
|
title: 'commit ai tells',
|
|
55
|
-
description: 'the
|
|
82
|
+
description: 'the commit-msg hook that refuses AI attribution is armed, and no ref name tells',
|
|
56
83
|
owner: 'identity/how-we-commit.md',
|
|
57
|
-
|
|
58
|
-
// describes, so
|
|
59
|
-
inputs: [CONFIG_FILE],
|
|
60
|
-
|
|
84
|
+
runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
|
|
85
|
+
// git config and ref names, which no tree id describes, so they are the key
|
|
86
|
+
cache: { by: 'files', inputs: [CONFIG_FILE], key: historyKey },
|
|
87
|
+
fix: 'none',
|
|
88
|
+
applies: 'always',
|
|
61
89
|
run: async (c) => {
|
|
62
90
|
const nested = nestedIn(c.root)
|
|
63
|
-
if (nested != null)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
91
|
+
if (nested != null) return void c.lines.push({ kind: 'skip', text: `commit gates not checked, nested in ${nested}: the parent repo owns them` })
|
|
92
|
+
const top = await c.exec(['git', 'rev-parse', '--show-toplevel'], { cwd: c.root })
|
|
93
|
+
if (top.code !== 0) return void c.lines.push({ kind: 'skip', text: 'not a git repo: no commit to guard yet' })
|
|
94
|
+
|
|
95
|
+
const hp = await c.exec(['git', 'config', 'core.hooksPath'], { cwd: c.root })
|
|
96
|
+
const dir = hp.code === 0 ? hp.out.trim() : ''
|
|
97
|
+
const chain = guardedChain(hookBinInvocation(c.root))
|
|
98
|
+
if (dir === '' && process.env['CI'] != null && process.env['CI'] !== '') {
|
|
99
|
+
c.lines.push({ kind: 'skip', text: 'commit-msg hook not armed in this CI checkout: a CI run makes no commit, and the fix pass arms hooks on a machine only' })
|
|
100
|
+
} else if (dir === '') {
|
|
101
|
+
c.fail('the commit-msg hook is not armed, so nothing refuses an AI attribution trailer: run `shipkit hooks install` (`shipkit ci` arms it too)')
|
|
102
|
+
} else {
|
|
103
|
+
const body = isAbsolute(dir) ? readOrNull(join(dir, 'commit-msg')) : c.read(`${dir}/commit-msg`)
|
|
104
|
+
const own = dir === LAYOUT.hooks
|
|
105
|
+
if (body == null)
|
|
106
|
+
c.fail(`core.hooksPath=${dir} has no ${dir}/commit-msg, so nothing refuses an AI attribution trailer: ${own ? 'run `shipkit hooks install`' : `add one that runs \`${chain}\``}`)
|
|
107
|
+
else if (!chainsShipkit(body))
|
|
108
|
+
c.fail(`core.hooksPath=${dir}: its commit-msg (${body.split('\n').find((l) => l.trim() !== '' && !l.startsWith('#'))?.trim().slice(0, 80) ?? 'empty'}) does not run the shipkit gate, so nothing refuses AI attribution: add \`${chain}\` above its last line`)
|
|
109
|
+
else c.ok(`the commit-msg hook refuses AI attribution (${own ? dir : `${dir}, chained to the shipkit gate`})`)
|
|
74
110
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// a checkpoint/session shape
|
|
93
|
-
const refTell = /\b(?:claude|chatgpt|copilot|aider|devin)\b|(?:^|[-_/])(?:fable|sonnet|haiku|opus|gemini|codex)[-_](?:checkpoint|session|ckpt)/i
|
|
94
|
-
const refs = await c.exec(['git', 'for-each-ref', '--format=%(refname:short)', 'refs/tags', 'refs/heads', 'refs/remotes'], { cwd: c.root, timeoutMs: 30_000 })
|
|
95
|
-
if (refs.code !== 0) c.lines.push({ kind: 'skip', text: `ref names unverifiable — git for-each-ref exit ${refs.code}` })
|
|
96
|
-
const tellRefs = refs.code === 0 ? refs.out.split('\n').map((r) => r.trim()).filter((r) => r !== '' && refTell.test(r)) : []
|
|
97
|
-
if (tellRefs.length > 0)
|
|
98
|
-
c.fail(`${tellRefs.length} ref name(s) carry an AI tell in a declared-public repo (${tellRefs.slice(0, 5).join(' ')}${tellRefs.length > 5 ? ' …' : ''}) — rename or delete the refs before the flip; a pushed tag also needs the remote delete`)
|
|
99
|
-
if (attributed.length > 0)
|
|
100
|
-
c.fail(`${attributed.length}/${commits.length} commits carry AI attribution (a trailer naming an assistant / a session trailer or claude.ai url / "Generated with" / 🤖) in a declared-public repo — e.g. ${sample(attributed)}; ${remedy}`)
|
|
101
|
-
if (dashed.length > 0)
|
|
102
|
-
c.fail(`${dashed.length}/${commits.length} commit messages carry dash punctuation (em/en dash or inline " - ") in a declared-public repo — e.g. ${sample(dashed)}; ${remedy} (identity/writing-voice.md owns the punctuation rule)`)
|
|
103
|
-
const silenced = attributionSilenced(c.root)
|
|
104
|
-
if (!silenced)
|
|
105
|
-
c.fail(`.claude/settings.json does not silence AI attribution — track it with {"attribution": {"commit": "", "pr": ""}} so no agent's harness ever writes the trailer or the PR footer`)
|
|
106
|
-
if (attributed.length === 0 && dashed.length === 0 && tellRefs.length === 0 && silenced)
|
|
107
|
-
c.ok(`${commits.length} commits + ${refs.code === 0 ? refs.out.split('\n').filter((r) => r.trim() !== '').length : 0} ref names scanned, attribution silenced in .claude/settings.json — no AI tells`)
|
|
111
|
+
|
|
112
|
+
if (declaredVisibility(c.config) !== 'public') return
|
|
113
|
+
if (!attributionSilenced(c.root))
|
|
114
|
+
c.fail(`.claude/settings.json does not silence AI attribution: track it with {"attribution": {"commit": "", "pr": ""}} so no agent's harness ever writes the trailer or the PR footer`)
|
|
115
|
+
const refs = await c.exec(['git', 'for-each-ref', '--format=%(refname)', 'refs/tags', 'refs/heads', 'refs/remotes'], { cwd: c.root, timeoutMs: 30_000 })
|
|
116
|
+
if (refs.code !== 0) return void c.lines.push({ kind: 'skip', text: `ref names unverifiable: git for-each-ref exit ${refs.code}` })
|
|
117
|
+
const all = refs.out.split('\n').map((r) => r.trim()).filter((r) => r !== '')
|
|
118
|
+
const remote = new Set(all.filter((r) => r.startsWith('refs/remotes/')).map((r) => r.split('/').slice(3).join('/')))
|
|
119
|
+
const tells = all.filter((r) => REF_TELL.test(r.replace(/^refs\/(?:heads|tags|remotes)\//, '')))
|
|
120
|
+
const shown = (r: string): string => r.replace(/^refs\/(?:heads|tags|remotes)\//, '')
|
|
121
|
+
const local = tells.filter((r) => r.startsWith('refs/heads/') && !remote.has(shown(r)))
|
|
122
|
+
const out = tells.filter((r) => !local.includes(r) && !(r.startsWith('refs/heads/') && remote.has(shown(r))))
|
|
123
|
+
const list = (rs: string[], suffix = ''): string => rs.slice(0, 5).map((r) => `${shown(r)}${suffix}`).join(' ') + (rs.length > 5 ? ' …' : '')
|
|
124
|
+
if (out.length > 0)
|
|
125
|
+
c.fail(`${out.length} ref name(s) carry an AI tell in a declared-public repo (${list(out)}): rename or delete them, a pushed one on the remote too, then \`git fetch --prune\` so a remote ref deleted there leaves this clone`)
|
|
126
|
+
if (local.length > 0)
|
|
127
|
+
c.warn(`${local.length} branch name(s) carry an AI tell (${list(local, ' (local only)')}): never push them under this name, rename or delete them`)
|
|
108
128
|
},
|
|
109
129
|
})
|
|
110
130
|
|
|
@@ -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))
|
|
@@ -7,16 +7,17 @@
|
|
|
7
7
|
//
|
|
8
8
|
// The tier split is the whole design. `fix` and `hard` hits BLOCK: they are
|
|
9
9
|
// mechanical, and a machine reporting them is never guessing. `soft` hits
|
|
10
|
-
// (sentence length, passive voice) need a human read,
|
|
11
|
-
//
|
|
12
|
-
// down.
|
|
10
|
+
// (sentence length, passive voice, modals, semicolons, …) need a human read,
|
|
11
|
+
// so they are a note, never an attention item, and only a declared
|
|
12
|
+
// `baseline` makes them blocking, as a ratchet that can only go down.
|
|
13
13
|
//
|
|
14
14
|
// It carries NO `fix()` on purpose. `shipkit ci` runs `--fix-rules` by
|
|
15
15
|
// default, so a rule with a fixer rewrites tracked files unattended, and
|
|
16
16
|
// prose is not repo shape: a wrong package.json line is a config, a wrong
|
|
17
17
|
// sentence is content. The rewrite lives behind an explicit `shipkit prose
|
|
18
18
|
// --fix` that a human ran and can read the diff of.
|
|
19
|
-
import { classSummary, proseLines, runProseCheck } from '../prose/run-prose.ts'
|
|
19
|
+
import { classSummary, proseLines, runProseCheck, type ProseReport } from '../prose/run-prose.ts'
|
|
20
|
+
import { tierOf, type ProseClass } from '../prose/ste.ts'
|
|
20
21
|
import { check } from '../check.ts'
|
|
21
22
|
|
|
22
23
|
const OWNER = 'identity/writing-voice.md'
|
|
@@ -29,6 +30,10 @@ export const proseSte = check({
|
|
|
29
30
|
owner: OWNER,
|
|
30
31
|
// keyed on what it reads: each include glob's listing and every file in
|
|
31
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',
|
|
32
37
|
run: (c) => {
|
|
33
38
|
const cfg = c.config.prose
|
|
34
39
|
if (cfg == null || (cfg.include ?? []).length === 0) {
|
|
@@ -53,12 +58,21 @@ export const proseSte = check({
|
|
|
53
58
|
`${blocking} STE violation(s) across ${r.byFile.length}/${r.files} file(s) — ${classSummary(r)}`,
|
|
54
59
|
r.fixable > 0 ? `shipkit prose --fix rewrites ${r.fixable} of them` : undefined,
|
|
55
60
|
)
|
|
61
|
+
const soft = softSummary(r)
|
|
56
62
|
if (cfg.baseline != null && r.byTier.soft > cfg.baseline)
|
|
57
|
-
c.fail(`${r.byTier.soft} soft hits (
|
|
63
|
+
c.fail(`${r.byTier.soft} soft hits (${soft}), above the declared baseline of ${cfg.baseline}: the ratchet only goes down`)
|
|
58
64
|
else if (r.byTier.soft > 0)
|
|
59
|
-
c.
|
|
65
|
+
c.note(`${r.byTier.soft} soft hit(s) need a human read (${soft})${cfg.baseline != null ? `, within the baseline of ${cfg.baseline}` : ''}: \`shipkit prose\` lists them`)
|
|
60
66
|
if (!c.failed && blocking === 0) c.ok(`${r.files} file(s) STE-clean on the mechanical rules`)
|
|
61
67
|
}
|
|
62
68
|
})
|
|
63
69
|
|
|
70
|
+
/** every soft class with its count, most first */
|
|
71
|
+
const softSummary = (r: ProseReport): string =>
|
|
72
|
+
Object.entries(r.byClass)
|
|
73
|
+
.filter(([cls]) => tierOf(cls as ProseClass) === 'soft')
|
|
74
|
+
.sort((a, b) => b[1] - a[1])
|
|
75
|
+
.map(([cls, n]) => `${cls} ${n}`)
|
|
76
|
+
.join(', ')
|
|
77
|
+
|
|
64
78
|
export default proseSte
|
|
@@ -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
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// SK016 — the shapes the check file replaced, and the machine that
|
|
2
2
|
// rewrites them (build/shipkit/writing-a-check.md owns the shape itself).
|
|
3
3
|
//
|
|
4
|
-
// The window: every shape below is READ
|
|
5
|
-
// refused at the first release after `shipkit fleet` shows no repo below 0.40
|
|
4
|
+
// The window: every shape below is still READ, named once per run, and
|
|
5
|
+
// refused at the first release after `shipkit fleet` shows no repo below 0.40
|
|
6
|
+
// and none still carrying a check script.
|
|
6
7
|
// So a repo on an old declaration keeps working, learns what to write instead
|
|
7
8
|
// on every gate, and is carried across by one command, save what only a
|
|
8
9
|
// reader can port: a check script's body, a rule, a task whose body is code.
|
|
@@ -48,6 +49,10 @@ export const legacyCheckShapes = check({
|
|
|
48
49
|
blocking: true,
|
|
49
50
|
// keyed on what it reads: the listing of check files, their sources, and
|
|
50
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',
|
|
51
56
|
run: (c) => {
|
|
52
57
|
const s = legacyShapes(c, c.config)
|
|
53
58
|
if (s.count === 0 && s.placeholders.length === 0) {
|
|
@@ -60,7 +65,7 @@ export const legacyCheckShapes = check({
|
|
|
60
65
|
if (s.placeholders.length > 0)
|
|
61
66
|
c.warn(
|
|
62
67
|
`${s.placeholders.length} check file(s) still open with the TODO description the migration wrote: ${s.placeholders.join(', ')}`,
|
|
63
|
-
'replace each TODO with one line stating what must be true',
|
|
68
|
+
'by hand: replace each TODO with one line stating what must be true',
|
|
64
69
|
)
|
|
65
70
|
if (s.count === 0) return
|
|
66
71
|
// one WARN per shape, never a bare line: the gate promises every line
|
|
@@ -69,7 +74,7 @@ export const legacyCheckShapes = check({
|
|
|
69
74
|
const head = s.checkFiles.slice(0, 3).map((f) => f.rel).join(', ')
|
|
70
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` : ''}`)
|
|
71
76
|
const off = s.checkFiles.filter((f) => !f.read.gate).length
|
|
72
|
-
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`)
|
|
73
78
|
}
|
|
74
79
|
if (s.checkDefaults) c.warn(`repo.config.ts checkDefaults lets a file forget \`blocking\` — the value belongs in each file`)
|
|
75
80
|
if (s.tasks.length > 0) {
|
|
@@ -77,13 +82,18 @@ export const legacyCheckShapes = check({
|
|
|
77
82
|
c.warn(`${s.tasks.length} \`tasks: []\` entr(ies), ${data} of them pure data — each one is a *.check.ts file with \`build: true\``)
|
|
78
83
|
}
|
|
79
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\``)
|
|
80
|
-
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)`)
|
|
86
|
+
// what the fixer rewrites without guessing: the exemptions, the tasks
|
|
87
|
+
// once all of them are data, checkDefaults once no script leans on it
|
|
88
|
+
const mechanical = (s.tasks.every((x) => x.data) ? s.tasks.length : 0) + (s.checkDefaults && s.checkFiles.length === 0 ? 1 : 0)
|
|
81
89
|
c.warn(
|
|
82
|
-
`${s.count} declaration(s) in a shape the check file replaced
|
|
83
|
-
|
|
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`,
|
|
91
|
+
mechanical > 0
|
|
92
|
+
? `shipkit check --fix SK016 rewrites ${mechanical} of them and names the rest`
|
|
93
|
+
: `nothing left is mechanical, so the port is by hand: each line above becomes a *.check.ts file (${OWNER})`,
|
|
84
94
|
)
|
|
85
95
|
},
|
|
86
|
-
|
|
96
|
+
repair: async (c) => await fixLegacyShapes(c),
|
|
87
97
|
})
|
|
88
98
|
|
|
89
99
|
export default legacyCheckShapes
|
|
@@ -156,17 +166,14 @@ export async function fixLegacyShapes(c: CheckCtx, rewrite: ConfigRewriter = inO
|
|
|
156
166
|
for (const r of config.rules ?? [])
|
|
157
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)`)
|
|
158
168
|
|
|
159
|
-
// 4. the
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
const cfgPlan: ConfigPlan = { when: {}, drop: [], name: config.name }
|
|
164
|
-
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 }
|
|
165
173
|
if (s.checkDefaults && filesLeft === 0) cfgPlan.drop.push({ key: 'checkDefaults', why: 'every file now declares its own blocking' })
|
|
166
174
|
else if (s.checkDefaults) hand.push(`repo.config.ts \`checkDefaults\` stays until the ${filesLeft} file(s) above declare their own \`blocking\``)
|
|
167
175
|
if (tasksMove) cfgPlan.drop.push({ key: 'tasks', why: 'every task is a check file now' })
|
|
168
|
-
if (
|
|
169
|
-
if (Object.keys(cfgPlan.when).length > 0 || cfgPlan.drop.length > 0) {
|
|
176
|
+
if (cfgPlan.drop.length > 0) {
|
|
170
177
|
const out = await rewrite(cfgPlan)
|
|
171
178
|
did.push(...out.did)
|
|
172
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')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// SK019 — tests run through the generated test checks only (build/shipkit.md
|
|
2
|
-
//
|
|
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 `
|
|
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 []
|