rvlib-shipkit 0.38.1 → 1.0.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 +367 -16
- package/README.md +26 -14
- package/package.json +2 -2
- package/src/assets/gen/comfyts-gen.ts +27 -10
- package/src/assets/gen/generate-roll.ts +1 -1
- package/src/assets/kit-config.ts +6 -6
- package/src/assets/run-assets.ts +14 -10
- package/src/assets/server/studio-server.ts +6 -5
- package/src/check.ts +343 -0
- package/src/{rules/SK001-package-json-shape.ts → checks/SK001-package-json-shape.check.ts} +44 -46
- package/src/checks/SK002-contracts-fulfillness.check.ts +24 -0
- package/src/checks/SK003-claude-md-shape.check.ts +66 -0
- package/src/checks/SK004-gitignore-policy.check.ts +309 -0
- package/src/{rules/SK005-rvlib-versions.ts → checks/SK005-rvlib-versions.check.ts} +46 -32
- package/src/{rules/SK006-npm-publish-safety.ts → checks/SK006-npm-publish-safety.check.ts} +25 -24
- package/src/{rules/SK007-github-visibility.ts → checks/SK007-github-visibility.check.ts} +20 -14
- package/src/{rules/SK008-reviewer-agent.ts → checks/SK008-reviewer-agent.check.ts} +29 -14
- package/src/{rules/SK009-commit-ai-tells.ts → checks/SK009-commit-ai-tells.check.ts} +24 -21
- package/src/checks/SK010-undeclared-unit.check.ts +111 -0
- package/src/{rules/SK011-sibling-unit-ranges.ts → checks/SK011-sibling-unit-ranges.check.ts} +21 -20
- package/src/{rules/SK012-config-barrel-import.ts → checks/SK012-config-barrel-import.check.ts} +18 -23
- package/src/{rules/SK013-one-way-in-policy.ts → checks/SK013-one-way-in-policy.check.ts} +58 -50
- package/src/{rules/SK014-prose-ste.ts → checks/SK014-prose-ste.check.ts} +27 -13
- package/src/{rules/SK015-nested-repo.ts → checks/SK015-nested-repo.check.ts} +15 -12
- package/src/checks/SK016-legacy-check-shapes.check.ts +182 -0
- package/src/checks/SK017-vscode-hub-task.check.ts +197 -0
- package/src/checks/SK018-test-files-run.check.ts +28 -0
- package/src/checks/SK019-tests-run-in-units.check.ts +76 -0
- package/src/checks/SK020-self-spawning-check.check.ts +147 -0
- package/src/checks/contracts.check.ts +20 -0
- package/src/cli/cmd_Asks.ts +3 -2
- package/src/cli/cmd_Assets.ts +2 -2
- package/src/cli/cmd_Bump.ts +1 -1
- package/src/cli/cmd_Check.ts +6 -6
- package/src/cli/cmd_Checks.ts +95 -0
- package/src/cli/cmd_Ci.ts +48 -13
- package/src/cli/cmd_Commands.ts +2 -6
- package/src/cli/cmd_ConfigKeys.ts +69 -0
- package/src/cli/cmd_Contracts.ts +1 -1
- package/src/cli/cmd_Fleet.ts +6 -2
- package/src/cli/cmd_Hooks.ts +1 -1
- package/src/cli/cmd_Keys.ts +43 -18
- package/src/cli/cmd_Mv.ts +2 -2
- package/src/cli/cmd_Prose.ts +10 -1
- package/src/cli/cmd_Releases.ts +2 -2
- package/src/cli/cmd_Serve.ts +1 -1
- package/src/cli/cmd_Start.ts +6 -6
- package/src/cli/cmd_Status.ts +25 -0
- package/src/cli/cmd_Tui.ts +2 -2
- package/src/cli.ts +7 -4
- package/src/collections/actions.ts +4 -4
- package/src/collections/checks.ts +258 -0
- package/src/collections/collection-types.ts +254 -1
- package/src/collections/comfy.ts +23 -0
- package/src/collections/md-body.ts +14 -0
- package/src/collections/pings.ts +38 -12
- package/src/collections/posts.ts +19 -3
- package/src/collections/providers.ts +198 -91
- package/src/collections/reflections.ts +41 -0
- package/src/collections/state-rules.ts +73 -0
- package/src/collections/test-time.ts +288 -0
- package/src/collections/tests.ts +181 -0
- package/src/collections/wikis.ts +66 -0
- package/src/comfy/comfy-panel.ts +292 -0
- package/src/contracts/apply-contracts.ts +17 -12
- package/src/contracts/contract-types.ts +16 -4
- package/src/contracts/contracts-engine.ts +78 -42
- package/src/contracts/manifests/discord.contract.ts +6 -6
- package/src/contracts/manifests/identity.contract.ts +3 -3
- package/src/contracts/manifests/itch.contract.ts +12 -15
- package/src/contracts/manifests/social.contract.ts +5 -5
- package/src/contracts/manifests/steam.contract.ts +16 -16
- package/src/contracts/run-contracts.ts +4 -3
- package/src/core/attach.ts +96 -0
- package/src/core/canonical-scripts.ts +25 -0
- package/src/core/check-ctx.ts +204 -14
- package/src/core/check-file-names.ts +112 -0
- package/src/core/check-pass.ts +84 -0
- package/src/core/check-runner.ts +796 -0
- package/src/core/config-keys.ts +256 -0
- package/src/core/config-rewrite.ts +77 -0
- package/src/core/discover-checks.ts +166 -90
- package/src/core/discover-legacy-checks.ts +26 -0
- package/src/core/doc-block.ts +13 -0
- package/src/core/floor-resolve.ts +264 -0
- package/src/core/floor.ts +20 -137
- package/src/core/git-bin.ts +24 -0
- package/src/core/git-root.ts +108 -0
- package/src/core/instance.ts +195 -0
- package/src/core/jobs.ts +118 -0
- package/src/core/layout.ts +52 -0
- package/src/core/legacy-shapes.ts +207 -0
- package/src/core/lib-root.ts +5 -0
- package/src/core/locked-write.ts +110 -39
- package/src/core/machine-commit.ts +5 -4
- package/src/core/migrate-layout.ts +557 -0
- package/src/core/motherhome.ts +2 -8
- package/src/core/nesting.ts +4 -12
- package/src/core/one-way-in-policy.ts +73 -17
- package/src/core/outcome-words.ts +45 -0
- package/src/core/package-manager.ts +86 -0
- package/src/core/parallel.ts +21 -0
- package/src/core/process-tree.ts +44 -0
- package/src/core/raw-access.ts +93 -0
- package/src/core/read-set.ts +152 -0
- package/src/core/run-check.ts +155 -213
- package/src/core/run-ci.ts +153 -189
- package/src/core/run-fix.ts +96 -0
- package/src/core/run-fixers.ts +53 -30
- package/src/core/self-update.ts +9 -3
- package/src/core/slots.ts +83 -0
- package/src/core/spawn-timeout.ts +34 -3
- package/src/core/when-block.ts +401 -0
- package/src/core/when.ts +135 -0
- package/src/core/write-status.ts +5 -2
- package/src/hooks/leak-guard.ts +10 -8
- package/src/hooks/one-way-in-guard.ts +13 -5
- package/src/hooks/root-identity.ts +2 -1
- package/src/hooks/run-hooks.ts +35 -23
- package/src/init-repo.ts +39 -23
- package/src/ledger/check-stats.ts +155 -0
- package/src/ledger/check-table.ts +179 -0
- package/src/ledger/db.ts +255 -0
- package/src/ledger/gate-checks.ts +433 -0
- package/src/ledger/git-key.ts +116 -77
- package/src/ledger/git-scan.ts +96 -35
- package/src/ledger/ledger.ts +116 -42
- package/src/ledger/live-runs.ts +111 -0
- package/src/ledger/live-timeline.ts +97 -0
- package/src/ledger/repo-view.ts +73 -0
- package/src/ledger/run-log.ts +448 -0
- package/src/ledger/run-timeline.ts +173 -0
- package/src/ledger/store.ts +23 -12
- package/src/ledger/suite-log.ts +161 -0
- package/src/ledger/toolchain-identity.ts +7 -1
- package/src/ledger/world-cache.ts +111 -0
- package/src/mv/mv-md.ts +16 -6
- package/src/problems/extract.ts +182 -0
- package/src/problems/problem-board.ts +37 -0
- package/src/problems/problem-line.ts +77 -0
- package/src/prose/prose-view.ts +90 -0
- package/src/prose/run-prose.ts +60 -19
- package/src/prose/ste.ts +140 -24
- package/src/reflections/reflections-queue.ts +81 -0
- package/src/release/git-facts.ts +15 -9
- package/src/release/run-bump.ts +6 -5
- package/src/release/run-release.ts +25 -11
- package/src/release/run-releases.ts +2 -2
- package/src/release/unit-gate.ts +7 -54
- package/src/release/unit-resolve.ts +282 -0
- package/src/release/units.ts +33 -217
- package/src/repo-config.ts +155 -85
- package/src/rules/rule.ts +36 -96
- package/src/rules/untrack-generated.ts +5 -4
- package/src/scaffold/files/vscode-tasks.json +10 -31
- package/src/scaffold/jsonc-spans.ts +113 -0
- package/src/scaffold/project-scaffold.ts +16 -43
- package/src/scan.ts +98 -0
- package/src/serve/gate-trigger.ts +148 -0
- package/src/serve/health-schedule.ts +102 -0
- package/src/serve/hub-app/chart-view.ts +179 -0
- package/src/serve/hub-app/check-view.tsx +345 -0
- package/src/serve/hub-app/check-words.ts +164 -0
- package/src/serve/hub-app/checks-svc.ts +280 -0
- package/src/serve/hub-app/clock-svc.ts +33 -0
- package/src/serve/hub-app/col-card.tsx +23 -39
- package/src/serve/hub-app/cols-svc.ts +44 -20
- package/src/serve/hub-app/comfy-svc.ts +171 -0
- package/src/serve/hub-app/comfy-view.tsx +81 -0
- package/src/serve/hub-app/dur.ts +41 -0
- package/src/serve/hub-app/hash-route.ts +121 -0
- package/src/serve/hub-app/header-bar.tsx +117 -0
- package/src/serve/hub-app/health-svc.ts +23 -28
- package/src/serve/hub-app/help.tsx +29 -0
- package/src/serve/hub-app/hub-app.tsx +122 -119
- package/src/serve/hub-app/hub-st.ts +115 -42
- package/src/serve/hub-app/hub-types.ts +117 -2
- package/src/serve/hub-app/hub.css +534 -192
- package/src/serve/hub-app/index.html +2 -0
- package/src/serve/hub-app/live-svc.ts +174 -0
- package/src/serve/hub-app/md.ts +15 -0
- package/src/serve/hub-app/media-svc.ts +4 -1
- package/src/serve/hub-app/media-view.tsx +15 -0
- package/src/serve/hub-app/next-run.ts +68 -0
- package/src/serve/hub-app/project-gutter.css +65 -0
- package/src/serve/hub-app/project-gutter.tsx +128 -0
- package/src/serve/hub-app/projects-svc.ts +226 -0
- package/src/serve/hub-app/prose-hits-st.ts +124 -0
- package/src/serve/hub-app/prose-hits-view.tsx +176 -0
- package/src/serve/hub-app/prose-hits.css +21 -0
- package/src/serve/hub-app/run-strip.ts +47 -0
- package/src/serve/hub-app/runs-tabs.ts +24 -0
- package/src/serve/hub-app/test-view.tsx +107 -0
- package/src/serve/hub-app/tests-svc.ts +62 -0
- package/src/serve/hub-app/tests-view.tsx +238 -0
- package/src/serve/hub-app/time-ago.tsx +34 -0
- package/src/serve/hub-app/timeline-chart.tsx +274 -0
- package/src/serve/hub-app/timeline-geometry.ts +314 -0
- package/src/serve/hub-app/timeline-svc.ts +62 -0
- package/src/serve/hub-app/timeline-view.tsx +245 -0
- package/src/serve/hub-app/tree-build.ts +410 -0
- package/src/serve/hub-app/tree-row.tsx +42 -0
- package/src/serve/hub-app/tree-svc.ts +174 -0
- package/src/serve/hub-app/wikis-view.tsx +53 -0
- package/src/serve/other-repos.ts +79 -0
- package/src/serve/project-gutter-route.ts +350 -0
- package/src/serve/project-gutter.ts +449 -0
- package/src/serve/run-serve.ts +245 -16
- package/src/serve/same-origin.ts +14 -0
- package/src/serve/serve-hub.ts +213 -18
- package/src/shipkit.ts +19 -12
- package/src/smoke/run-smoke.ts +3 -2
- package/src/start-repo.ts +75 -36
- package/src/suite/judge-suites.ts +209 -15
- package/src/suite/kept-timings.ts +58 -0
- package/src/suite/runner-shapes.ts +88 -0
- package/src/suite/suite-cli.ts +147 -24
- package/src/suite/suite-cpu-preload.ts +92 -0
- package/src/suite/test-units.ts +243 -0
- package/src/ts/config.ts +4 -69
- package/src/ts/jsonc.ts +69 -0
- package/src/ts/lex.ts +143 -0
- package/src/ts/rename.ts +73 -10
- package/src/ts/source-graph.ts +28 -8
- package/src/ts/tsconfig-projects.ts +6 -97
- package/src/ts/tsconfig-resolve.ts +108 -0
- package/src/tui/build-steps-panel.ts +61 -0
- package/src/tui/checks-panel.ts +24 -16
- package/src/tui/run-tui.ts +2 -2
- package/src/tui/tui-panel.ts +5 -4
- package/src/wiki/graph-colors.ts +68 -0
- package/src/wiki/graph-mount.ts +7 -9
- package/src/wiki/graph-page.ts +42 -25
- package/src/wiki/graph-positions.ts +105 -0
- package/src/wiki/run-wiki.ts +1 -1
- package/src/wiki/serve-graph.ts +33 -3
- package/src/cli/cmd_Rules.ts +0 -51
- package/src/cli/cmd_Units.ts +0 -82
- package/src/collections/graphs.ts +0 -32
- package/src/contracts/contracts-once.ts +0 -43
- package/src/core/check-suite.ts +0 -302
- package/src/core/repair-contract-dirs.ts +0 -136
- package/src/core/task-runner.ts +0 -339
- package/src/ledger/gate-units.ts +0 -138
- package/src/ledger/unit-table.ts +0 -105
- package/src/rules/SK002-contracts-fulfillness.ts +0 -24
- package/src/rules/SK003-claude-md-shape.ts +0 -70
- package/src/rules/SK004-gitignore-policy.ts +0 -238
- package/src/rules/SK010-undeclared-release-unit.ts +0 -127
- package/src/rules/rule-registry.ts +0 -64
- package/src/rules/run-fix.ts +0 -72
- package/src/tui/tasks-panel.ts +0 -60
|
@@ -14,50 +14,56 @@
|
|
|
14
14
|
// erroring, unreadable answer) is a loud skip — an UNKNOWN, never red,
|
|
15
15
|
// never cached.
|
|
16
16
|
import { carveGhJson, firstGhLine } from '../git-bootstrap.ts'
|
|
17
|
+
import { probeGitRoot } from '../core/git-root.ts'
|
|
17
18
|
import { nestedIn } from '../core/nesting.ts'
|
|
18
|
-
import
|
|
19
|
+
import { check } from '../check.ts'
|
|
19
20
|
|
|
20
|
-
export const githubVisibility
|
|
21
|
+
export const githubVisibility = check({
|
|
21
22
|
id: 'SK007',
|
|
22
23
|
// blocking despite being measured over the network: the fact is about THIS
|
|
23
24
|
// REPO (it is public while it declares itself private), and every
|
|
24
25
|
// unverifiable path above is a skip, so an offline machine never goes red
|
|
25
26
|
blocking: true,
|
|
26
27
|
title: 'github visibility',
|
|
28
|
+
description: 'the github visibility of this repo matches the one it declares',
|
|
27
29
|
owner: 'meta/contracts.md',
|
|
28
30
|
ttl: '7d',
|
|
29
31
|
cacheKey: (env) => env.config.visibility ?? 'private',
|
|
30
|
-
|
|
31
|
-
const declared =
|
|
32
|
-
const skip = (why: string): void => void
|
|
32
|
+
run: async (c) => {
|
|
33
|
+
const declared = c.config.visibility ?? 'private'
|
|
34
|
+
const skip = (why: string): void => void c.lines.push({ kind: 'skip', text: `github visibility unverified — ${why} (declared: ${declared})` })
|
|
33
35
|
const firstLine = firstGhLine
|
|
34
|
-
const
|
|
36
|
+
const repo = probeGitRoot(c.root)
|
|
37
|
+
if (repo.root == null) return skip(`git says: ${firstLine(repo.error)}`)
|
|
38
|
+
const nested = nestedIn(c.root)
|
|
35
39
|
if (nested != null)
|
|
36
|
-
return void
|
|
40
|
+
return void c.lines.push({ kind: 'skip', text: `github visibility unverified, nested in ${nested}: the remote belongs to the parent repo (declared: ${declared})` })
|
|
37
41
|
|
|
38
|
-
const remotes = await
|
|
42
|
+
const remotes = await c.exec(['git', 'remote'], { cwd: c.root })
|
|
39
43
|
if (remotes.code !== 0) return skip(`git says: ${firstLine(remotes.out) || `exit ${remotes.code}`}`)
|
|
40
44
|
const names = remotes.out.trim().split('\n').filter((l) => l !== '')
|
|
41
45
|
if (names.length === 0) return skip('no git remote')
|
|
42
46
|
// prefer a `github`-named remote over `origin` when both exist
|
|
43
47
|
const pick = ['github', 'origin'].find((n) => names.includes(n)) ?? names[0]!
|
|
44
|
-
const url = await
|
|
48
|
+
const url = await c.exec(['git', 'remote', 'get-url', pick], { cwd: c.root })
|
|
45
49
|
if (url.code !== 0) return skip(`git says: ${firstLine(url.out)}`)
|
|
46
50
|
const m = url.out.match(/github\.com[:/]([^/\s]+\/[^/\s]+?)(?:\.git)?\s*$/m)
|
|
47
51
|
if (m == null) return skip(`remote '${pick}' is not github (${firstLine(url.out)})`)
|
|
48
52
|
const target = m[1]!
|
|
49
53
|
|
|
50
|
-
const r = await
|
|
54
|
+
const r = await c.exec(['gh', 'repo', 'view', target, '--json', 'isPrivate,nameWithOwner'], { cwd: c.root, timeoutMs: 30_000 })
|
|
51
55
|
if (r.code !== 0) return skip(`gh failed (${firstLine(r.out)})`)
|
|
52
56
|
// an unreadable answer stays an UNKNOWN, never a FAIL
|
|
53
57
|
const parsed = carveGhJson<{ isPrivate?: boolean; nameWithOwner?: string }>(r.out)
|
|
54
58
|
if (parsed == null) return skip(`gh answer unreadable (${firstLine(r.out)})`)
|
|
55
59
|
const actual = parsed.isPrivate === true ? 'private' : 'public'
|
|
56
60
|
const who = parsed.nameWithOwner ?? target
|
|
57
|
-
if (actual === declared)
|
|
61
|
+
if (actual === declared) c.ok(`github ${who} is ${actual} — matches repo.config visibility`)
|
|
58
62
|
else if (actual === 'public')
|
|
59
|
-
|
|
63
|
+
c.fail(`github ${who} is PUBLIC but repo.config declares '${declared}' — the leak case; make it private again or run the pre-public gate and declare visibility: 'public'`)
|
|
60
64
|
else
|
|
61
|
-
|
|
65
|
+
c.fail(`github ${who} is private but repo.config declares 'public' — stale config lying to agents; fix the declaration`)
|
|
62
66
|
},
|
|
63
|
-
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
export default githubVisibility
|
|
@@ -11,9 +11,14 @@ import { existsSync, mkdirSync, readFileSync } from 'fs'
|
|
|
11
11
|
import { homedir } from 'os'
|
|
12
12
|
import { dirname, isAbsolute, join } from 'path'
|
|
13
13
|
import type { RepoConfig } from '../repo-config.ts'
|
|
14
|
-
import
|
|
14
|
+
import { check, type FixResult, type CheckEnv } from '../check.ts'
|
|
15
|
+
import { LAYOUT } from '../core/layout.ts'
|
|
15
16
|
import { atomicWrite } from '../core/locked-write.ts'
|
|
16
17
|
|
|
18
|
+
/** where a review lands when its caller named no output file:
|
|
19
|
+
* `YYYYMMDD-HHMMSS-<review-name>.md`, local time, gitignored */
|
|
20
|
+
export const REVIEWS_DIR = LAYOUT.reviews
|
|
21
|
+
|
|
17
22
|
/** default read-only toolset — an ALLOWLIST, not a denylist: a tool that can
|
|
18
23
|
* edit by proxy (spawning a subagent, an MCP writer) is exactly what a
|
|
19
24
|
* denylist of today's edit-tool names would wave through */
|
|
@@ -60,6 +65,13 @@ purpose: your product is findings, never fixes. Bash is for READING the state
|
|
|
60
65
|
of things (\`git diff\`, the gates, running a claim to check it) — never for
|
|
61
66
|
writing files by another route.
|
|
62
67
|
|
|
68
|
+
ONE exception: your review itself. Unless the caller named an output file,
|
|
69
|
+
write it with one Bash heredoc to
|
|
70
|
+
\`${REVIEWS_DIR}/YYYYMMDD-HHMMSS-<review-name>.md\` (local time, 24h, a
|
|
71
|
+
kebab name for what you reviewed; \`mkdir -p\` first), then return the same
|
|
72
|
+
findings to the caller. The file is the copy that outlives the conversation,
|
|
73
|
+
gitignored.
|
|
74
|
+
|
|
63
75
|
FIRST ACTION, always: read \`${reviewer.brief}\` and follow it exactly. It tells
|
|
64
76
|
you what else to read, when to refuse the job, the order of the questions, and
|
|
65
77
|
the output shape.
|
|
@@ -68,36 +80,39 @@ That brief overrides any habit of reviewing a diff for correctness alone.
|
|
|
68
80
|
`
|
|
69
81
|
}
|
|
70
82
|
|
|
71
|
-
export const reviewerAgent
|
|
83
|
+
export const reviewerAgent = check({
|
|
72
84
|
id: 'SK008',
|
|
73
85
|
blocking: true,
|
|
74
86
|
title: 'reviewer agent',
|
|
87
|
+
description: 'the declared reviewer agent has its brief, its definition and read only tools',
|
|
75
88
|
owner: 'identity/how-we-work.md',
|
|
76
89
|
// the brief is a CONFIG path, and a `~/...` or absolute one puts it
|
|
77
90
|
// outside the repo, where no tree id can see whether it still exists
|
|
78
91
|
never: 'the reviewer brief may live outside the repo, where no tree id reaches it',
|
|
79
|
-
|
|
80
|
-
const r =
|
|
81
|
-
if (r == null) return
|
|
92
|
+
run: (c) => {
|
|
93
|
+
const r = c.config.reviewer
|
|
94
|
+
if (r == null) return c.ok('no reviewer agent declared (repo.config reviewer:) — skipped')
|
|
82
95
|
const rel = reviewerAgentPath(r.name)
|
|
83
|
-
const p = join(
|
|
84
|
-
if (briefMissing(
|
|
85
|
-
if (!existsSync(p)) return
|
|
96
|
+
const p = join(c.root, rel)
|
|
97
|
+
if (briefMissing(c.root, r.brief)) c.fail(`the declared brief '${r.brief}' does not exist — every review's mandated first action would fail`)
|
|
98
|
+
if (!existsSync(p)) return c.fail(`${rel} missing — the declared reviewer type is unavailable, so any review runs on whatever framing the spawner improvised`)
|
|
86
99
|
const issues = reviewerAgentIssues(readFileSync(p, 'utf8'), r)
|
|
87
|
-
for (const i of issues)
|
|
88
|
-
if (issues.length === 0 && !
|
|
100
|
+
for (const i of issues) c.fail(`${rel}: ${i}`)
|
|
101
|
+
if (issues.length === 0 && !c.failed) c.ok(`${rel} → ${r.brief}`)
|
|
89
102
|
},
|
|
90
103
|
// create-if-missing only: an existing definition may carry a repo's own
|
|
91
104
|
// tuned prompt, and silently rewriting that would cost more than the
|
|
92
105
|
// finding it repairs. Violations in a present file stay a loud FAIL.
|
|
93
|
-
fix: (
|
|
94
|
-
const r =
|
|
106
|
+
fix: (c): FixResult => {
|
|
107
|
+
const r = c.config.reviewer
|
|
95
108
|
if (r == null) return { changed: false, summary: 'no reviewer declared — nothing to write' }
|
|
96
109
|
const rel = reviewerAgentPath(r.name)
|
|
97
|
-
const p = join(
|
|
110
|
+
const p = join(c.root, rel)
|
|
98
111
|
if (existsSync(p)) return { changed: false, summary: `${rel} exists — remaining findings are about its content, fix them by hand` }
|
|
99
112
|
mkdirSync(dirname(p), { recursive: true })
|
|
100
113
|
atomicWrite(p, canonicalAgent(r))
|
|
101
114
|
return { changed: true, summary: `${rel} written — points at ${r.brief}, no edit tools` }
|
|
102
115
|
},
|
|
103
|
-
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
export default reviewerAgent
|
|
@@ -12,7 +12,7 @@ import { readFileSync } from 'fs'
|
|
|
12
12
|
import { join } from 'path'
|
|
13
13
|
import { hasAiAttribution, hasDashPunctuation } from '../hooks/commit-message.ts'
|
|
14
14
|
import { nestedIn } from '../core/nesting.ts'
|
|
15
|
-
import
|
|
15
|
+
import { check } from '../check.ts'
|
|
16
16
|
|
|
17
17
|
// the documented silencer: .claude/settings.json attribution.commit/.pr both
|
|
18
18
|
// empty — the harness then never writes the trailer or the PR footer
|
|
@@ -25,27 +25,28 @@ const attributionSilenced = (root: string): boolean => {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export const commitAiTells
|
|
28
|
+
export const commitAiTells = check({
|
|
29
29
|
id: 'SK009',
|
|
30
30
|
blocking: true,
|
|
31
31
|
title: 'commit ai tells',
|
|
32
|
+
description: 'the history of a public repo carries no AI attribution and no dash punctuation',
|
|
32
33
|
owner: 'identity/how-we-commit.md',
|
|
33
34
|
// it scans the full `git log` and every ref name. History and refs are not
|
|
34
35
|
// work-tree content, so nothing a scan can produce describes this answer
|
|
35
36
|
never: 'reads the full commit history and every ref name, neither of which is work-tree content',
|
|
36
|
-
|
|
37
|
-
const nested = nestedIn(
|
|
37
|
+
run: async (c) => {
|
|
38
|
+
const nested = nestedIn(c.root)
|
|
38
39
|
if (nested != null)
|
|
39
|
-
return void
|
|
40
|
-
const declared =
|
|
40
|
+
return void c.lines.push({ kind: 'skip', text: `commit history not scanned, nested in ${nested}: that history belongs to the parent repo` })
|
|
41
|
+
const declared = c.config.visibility ?? 'private'
|
|
41
42
|
if (declared !== 'public')
|
|
42
|
-
return
|
|
43
|
+
return c.ok(`declared ${declared} — the no-AI-attribution ban still applies from commit one (identity/how-we-commit.md); the full-history scan arms at visibility: 'public'`)
|
|
43
44
|
// %x1f separates hash from body, %x1e terminates the record — commit
|
|
44
45
|
// bodies are free text, so only unprintable separators are safe
|
|
45
|
-
const log = await
|
|
46
|
+
const log = await c.exec(['git', 'log', '--format=%h%x1f%B%x1e'], { cwd: c.root, timeoutMs: 30_000 })
|
|
46
47
|
if (log.code !== 0) {
|
|
47
48
|
const first = log.out.trim().split('\n').find((l) => l.trim() !== '')?.slice(0, 120) ?? `exit ${log.code}`
|
|
48
|
-
return void
|
|
49
|
+
return void c.lines.push({ kind: 'skip', text: `commit history unverifiable — git says: ${first}` })
|
|
49
50
|
}
|
|
50
51
|
const commits = log.out
|
|
51
52
|
.split('\x1e')
|
|
@@ -55,30 +56,32 @@ export const commitAiTells: Rule = {
|
|
|
55
56
|
const cut = r.indexOf('\x1f')
|
|
56
57
|
return { hash: r.slice(0, cut), body: r.slice(cut + 1) }
|
|
57
58
|
})
|
|
58
|
-
if (commits.length === 0) return void
|
|
59
|
+
if (commits.length === 0) return void c.lines.push({ kind: 'skip', text: 'commit history unverifiable — no commits yet' })
|
|
59
60
|
const remedy = 'an unpushed TIP commit may be amended; anything deeper is the owner\'s decision (history is never rewritten as a side effect)'
|
|
60
61
|
const sample = (hits: { hash: string }[]): string => hits.slice(0, 5).map((h) => h.hash).join(' ') + (hits.length > 5 ? ' …' : '')
|
|
61
|
-
const attributed = commits.filter((
|
|
62
|
-
const dashed = commits.filter((
|
|
62
|
+
const attributed = commits.filter((m) => hasAiAttribution(m.body))
|
|
63
|
+
const dashed = commits.filter((m) => hasDashPunctuation(m.body))
|
|
63
64
|
// ref NAMES tell too (a FABLE-checkpoint-3 tag outs the agent as
|
|
64
65
|
// loudly as a trailer). bare match only for names with no other
|
|
65
66
|
// meaning; vendor and codename words (cursor, openai, gemini, opus,
|
|
66
67
|
// fable, …) name legitimate branches and features, they only match in
|
|
67
68
|
// a checkpoint/session shape
|
|
68
69
|
const refTell = /\b(?:claude|chatgpt|copilot|aider|devin)\b|(?:^|[-_/])(?:fable|sonnet|haiku|opus|gemini|codex)[-_](?:checkpoint|session|ckpt)/i
|
|
69
|
-
const refs = await
|
|
70
|
-
if (refs.code !== 0)
|
|
70
|
+
const refs = await c.exec(['git', 'for-each-ref', '--format=%(refname:short)', 'refs/tags', 'refs/heads', 'refs/remotes'], { cwd: c.root, timeoutMs: 30_000 })
|
|
71
|
+
if (refs.code !== 0) c.lines.push({ kind: 'skip', text: `ref names unverifiable — git for-each-ref exit ${refs.code}` })
|
|
71
72
|
const tellRefs = refs.code === 0 ? refs.out.split('\n').map((r) => r.trim()).filter((r) => r !== '' && refTell.test(r)) : []
|
|
72
73
|
if (tellRefs.length > 0)
|
|
73
|
-
|
|
74
|
+
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`)
|
|
74
75
|
if (attributed.length > 0)
|
|
75
|
-
|
|
76
|
+
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}`)
|
|
76
77
|
if (dashed.length > 0)
|
|
77
|
-
|
|
78
|
-
const silenced = attributionSilenced(
|
|
78
|
+
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)`)
|
|
79
|
+
const silenced = attributionSilenced(c.root)
|
|
79
80
|
if (!silenced)
|
|
80
|
-
|
|
81
|
+
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`)
|
|
81
82
|
if (attributed.length === 0 && dashed.length === 0 && tellRefs.length === 0 && silenced)
|
|
82
|
-
|
|
83
|
+
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`)
|
|
83
84
|
},
|
|
84
|
-
}
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
export default commitAiTells
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// SK010 — undeclared unit (build/shipkit/units.md). Units are
|
|
2
|
+
// DECLARED, never inferred, so resolution is deterministic. This rule is the
|
|
3
|
+
// other half of that deal: a package.json sitting in the repo that no unit
|
|
4
|
+
// claims would be invisible to every release surface, and its version would
|
|
5
|
+
// drift forever with nothing reporting it. Three declarations clear it, so the
|
|
6
|
+
// green is always reachable: `units: [{ dir }]` for a releasable package,
|
|
7
|
+
// `units: [{ dir, release: false }]` for one whose tests run in their own
|
|
8
|
+
// check, `notReleasable: [dir]` for one that ships inside something else. That second
|
|
9
|
+
// escape has teeth on purpose, because it is the one an agent staring at a red
|
|
10
|
+
// gate reaches for: an entry must cover at least one real package and never the
|
|
11
|
+
// whole repo, and the green counts the PACKAGES it excused, so a wide entry
|
|
12
|
+
// cannot read as a narrow one.
|
|
13
|
+
import { dirname, join, relative } from 'path'
|
|
14
|
+
import type { CheckCtx } from '../core/check-ctx.ts'
|
|
15
|
+
import { checkUnitReader, resolveUnits } from '../release/unit-resolve.ts'
|
|
16
|
+
import { check } from '../check.ts'
|
|
17
|
+
|
|
18
|
+
const SKIP = new Set(['node_modules', 'references', 'tmp', 'stats', 'dist', 'build'])
|
|
19
|
+
|
|
20
|
+
/** every package.json below the root that carries a name, root excluded, as
|
|
21
|
+
* absolute dirs. The files git sees, so a gitignored vendor tree or a python
|
|
22
|
+
* venv never trips the gate */
|
|
23
|
+
export function packageDirs(c: CheckCtx): string[] {
|
|
24
|
+
const named = c.list('**/package.json').filter((f) => {
|
|
25
|
+
if (f === 'package.json') return false
|
|
26
|
+
if (dirname(f).split('/').some((seg) => SKIP.has(seg) || seg.startsWith('.'))) return false
|
|
27
|
+
try {
|
|
28
|
+
const pkg = c.json<{ name?: string }>(f)
|
|
29
|
+
return typeof pkg?.name === 'string' && pkg.name !== ''
|
|
30
|
+
} catch { return false } // a broken package.json is SK001's story, not this rule's
|
|
31
|
+
})
|
|
32
|
+
return [...new Set(named.map((f) => join(c.root, dirname(f))))].sort()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const undeclaredUnit = check({
|
|
36
|
+
id: 'SK010',
|
|
37
|
+
blocking: true,
|
|
38
|
+
title: 'units declared',
|
|
39
|
+
description: 'every package.json is a declared unit or declared not releasable',
|
|
40
|
+
owner: 'build/shipkit/units.md',
|
|
41
|
+
// keyed on what it reads: the listing of every package.json, each one it
|
|
42
|
+
// opens, and the declared units' package.json
|
|
43
|
+
run: (c) => {
|
|
44
|
+
const { units, errors, testOnly } = resolveUnits(c.root, c.config, checkUnitReader(c))
|
|
45
|
+
// a DECLARED unit that does not resolve is a declaration bug and
|
|
46
|
+
// belongs here. A root package that is not release-shaped (no version
|
|
47
|
+
// yet) is not: the release path refuses on its own, the day it matters.
|
|
48
|
+
const declared = errors.filter((e) => e.id !== 'root')
|
|
49
|
+
if (declared.length > 0) {
|
|
50
|
+
for (const e of declared) c.fail(`declared unit '${e.id}' does not resolve: ${e.message}`, 'repo.config units[]')
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
// an entry is a PREFIX over the packages under it, so `examples/` can
|
|
54
|
+
// excuse examples/a and examples/b without enumerating them. Two shapes
|
|
55
|
+
// are refused, both being the kill switch this rule exists to prevent:
|
|
56
|
+
// an entry covering the whole repo, and one covering nothing today that
|
|
57
|
+
// stands ready to excuse whatever lands there tomorrow. defineRepo
|
|
58
|
+
// refuses the root too, and a repo.config.ts that never calls it is
|
|
59
|
+
// exactly why the check also lives in the blocking rule.
|
|
60
|
+
const decls = c.config.notReleasable ?? []
|
|
61
|
+
const covered = (list: string[], d: string): boolean => list.some((x) => d === x || d.startsWith(`${x}/`))
|
|
62
|
+
const pkgs = packageDirs(c)
|
|
63
|
+
const wide = decls.filter((d) => join(c.root, d) === c.root)
|
|
64
|
+
if (wide.length > 0) {
|
|
65
|
+
for (const d of wide)
|
|
66
|
+
c.fail(`notReleasable '${d}' resolves to the repo root, so it excuses every package at once`, `repo.config notReleasable: name one package dir (or one container dir) per entry`)
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
const empty = decls.filter((d) => !pkgs.some((p) => covered([join(c.root, d)], p)))
|
|
70
|
+
if (empty.length > 0) {
|
|
71
|
+
for (const d of empty)
|
|
72
|
+
c.fail(`notReleasable '${d}' covers no package.json, so it excuses no package today and every package that lands there tomorrow`, `repo.config notReleasable: point it at a package dir (or a dir holding some), or drop the entry`)
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
const excused = decls.map((d) => join(c.root, d))
|
|
76
|
+
const declaredDirs = units.filter((u) => u.rel !== '').map((u) => u.dir)
|
|
77
|
+
// an excuse that also covers a declared UNIT is the kill switch one
|
|
78
|
+
// level down: `notReleasable: ['libs']` beside `units: [libs/a]` reads
|
|
79
|
+
// as green while every new package under libs/ hides. The two lists
|
|
80
|
+
// describe opposite intents, so they must not overlap.
|
|
81
|
+
const overlapping = decls.filter((d) => declaredDirs.some((u) => u === join(c.root, d) || u.startsWith(`${join(c.root, d)}/`)))
|
|
82
|
+
if (overlapping.length > 0) {
|
|
83
|
+
for (const d of overlapping)
|
|
84
|
+
c.fail(`notReleasable '${d}' also covers a declared unit, so it excuses every package under it while reading as green`, `repo.config notReleasable: name the dirs that are NOT units, never a container holding one`)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
// a `release: false` unit already says what its dir is, so an excuse
|
|
88
|
+
// naming it too is a second statement of one fact, and the two drift
|
|
89
|
+
const doubled = decls.filter((d) => testOnly.some((u) => u.dir === join(c.root, d) || u.dir.startsWith(`${join(c.root, d)}/`)))
|
|
90
|
+
if (doubled.length > 0) {
|
|
91
|
+
for (const d of doubled)
|
|
92
|
+
c.fail(`notReleasable '${d}' also covers a unit declared release: false, which already claims its dir`, `repo.config notReleasable: drop '${d}', the units[] entry says it`)
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
const claimed = [...declaredDirs, ...testOnly.map((u) => u.dir), ...excused]
|
|
96
|
+
const orphans = pkgs.filter((d) => !covered(claimed, d))
|
|
97
|
+
if (orphans.length === 0) {
|
|
98
|
+
// PACKAGES, not entries: one entry over a nested tree excuses more
|
|
99
|
+
// than one, and the count is the only place that shows it
|
|
100
|
+
const n = pkgs.filter((d) => covered(excused, d)).length
|
|
101
|
+
const t = testOnly.length
|
|
102
|
+
return c.ok(`${units.length} releasing unit(s) declared${units.length === 1 ? ' (the repo root)' : ''}${t > 0 ? ` + ${t} test only unit(s)` : ''}${n > 0 ? ` + ${n} package(s) declared not releasable` : ''} — no package hides from a release`)
|
|
103
|
+
}
|
|
104
|
+
for (const d of orphans) {
|
|
105
|
+
const rel = relative(c.root, d)
|
|
106
|
+
c.fail(`${rel}/package.json is claimed by nothing — declare what it is, or its version drifts with nothing watching`, `repo.config units: [{ dir: '${rel}' }] if it releases, units: [{ dir: '${rel}', release: false }] if only its tests run, else notReleasable: ['${rel}']`)
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
export default undeclaredUnit
|
package/src/{rules/SK011-sibling-unit-ranges.ts → checks/SK011-sibling-unit-ranges.check.ts}
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// SK011 — a dependency range on a SIBLING
|
|
2
|
-
// the version that sibling actually carries (build/shipkit.md
|
|
1
|
+
// SK011 — a dependency range on a SIBLING unit must be satisfied by
|
|
2
|
+
// the version that sibling actually carries (build/shipkit/units.md).
|
|
3
3
|
//
|
|
4
4
|
// The failure it exists for: one unit is edited and its dependents keep a
|
|
5
5
|
// range that names a version nobody published, or an older one that lacks the
|
|
@@ -9,10 +9,9 @@
|
|
|
9
9
|
// interrupted.
|
|
10
10
|
//
|
|
11
11
|
// Local protocols (link:, file:) are SK006's story, not this rule's.
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import type { Rule } from './rule.ts'
|
|
12
|
+
import { join, relative } from 'path'
|
|
13
|
+
import { checkUnitReader, resolveUnits } from '../release/unit-resolve.ts'
|
|
14
|
+
import { check } from '../check.ts'
|
|
16
15
|
|
|
17
16
|
type Pkg = {
|
|
18
17
|
dependencies?: Record<string, string>
|
|
@@ -54,24 +53,24 @@ export function satisfiesSimple(version: string, range: string): boolean | null
|
|
|
54
53
|
return va === ra && vi === ri && vp === rp
|
|
55
54
|
}
|
|
56
55
|
|
|
57
|
-
export const siblingUnitRanges
|
|
56
|
+
export const siblingUnitRanges = check({
|
|
58
57
|
id: 'SK011',
|
|
59
58
|
blocking: true,
|
|
60
59
|
title: 'sibling unit ranges',
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
description: 'every range on a sibling unit floors at the version that sibling carries',
|
|
61
|
+
owner: 'build/shipkit/built-in-checks.md',
|
|
62
|
+
// keyed on what it reads: every declared unit's package.json, at the dirs
|
|
63
|
+
// the config picks
|
|
64
|
+
run: (c) => {
|
|
65
|
+
const { units } = resolveUnits(c.root, c.config, checkUnitReader(c))
|
|
66
66
|
const version = new Map(units.map((u) => [u.name, u.version]))
|
|
67
|
-
if (units.length < 2) return
|
|
67
|
+
if (units.length < 2) return c.ok('one releasing unit — no sibling ranges to keep honest')
|
|
68
68
|
const bad: string[] = []
|
|
69
69
|
let checked = 0
|
|
70
70
|
for (const u of units) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
try { pkg = JSON.parse(readFileSync(p, 'utf8')) as Pkg } catch { continue }
|
|
71
|
+
let pkg: Pkg | null
|
|
72
|
+
try { pkg = c.json<Pkg>(relative(c.root, join(u.dir, 'package.json'))) } catch { continue }
|
|
73
|
+
if (pkg == null) continue
|
|
75
74
|
for (const block of BLOCKS)
|
|
76
75
|
for (const [dep, range] of Object.entries(pkg[block] ?? {})) {
|
|
77
76
|
const actual = version.get(dep)
|
|
@@ -87,7 +86,9 @@ export const siblingUnitRanges: Rule = {
|
|
|
87
86
|
}
|
|
88
87
|
}
|
|
89
88
|
if (bad.length > 0)
|
|
90
|
-
|
|
91
|
-
else
|
|
89
|
+
c.fail(`${bad.join(' · ')} — a sibling range FLOORS at the version it names, so a stale floor lets a consumer keep an older copy and miss the release. \`shipkit bump\` moves every floor forward in the version commit, so a red here is a bump nobody propagated`)
|
|
90
|
+
else c.ok(`${checked} sibling range(s) floor at exactly the version each unit carries`)
|
|
92
91
|
},
|
|
93
|
-
}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
export default siblingUnitRanges
|
package/src/{rules/SK012-config-barrel-import.ts → checks/SK012-config-barrel-import.check.ts}
RENAMED
|
@@ -10,10 +10,7 @@
|
|
|
10
10
|
// A barrel is a fine PUBLIC surface and a terrible one to import from a config
|
|
11
11
|
// file that exists to be read before anything else happens. This rule is
|
|
12
12
|
// narrow on purpose: it is about repo.config.ts, not about barrels in general.
|
|
13
|
-
import {
|
|
14
|
-
import { join } from 'path'
|
|
15
|
-
import type { FixResult, Rule, RuleEnv } from './rule.ts'
|
|
16
|
-
import { atomicWrite } from '../core/locked-write.ts'
|
|
13
|
+
import { check, type FixResult } from '../check.ts'
|
|
17
14
|
|
|
18
15
|
const CONFIG = 'repo.config.ts'
|
|
19
16
|
const BARREL = /(^|\n)(\s*import\s+(?:type\s+)?\{[^}]*\}\s+from\s+)['"]rvlib-shipkit['"]/
|
|
@@ -27,38 +24,36 @@ const CONFIG_SYMBOLS = new Set(['defineRepo', 'RepoConfig', 'TaskDef', 'ServeCon
|
|
|
27
24
|
const importedSymbols = (line: string): string[] =>
|
|
28
25
|
(/\{([^}]*)\}/.exec(line)?.[1] ?? '').split(',').map((s) => s.replace(/^\s*type\s+/, '').trim()).filter((s) => s !== '')
|
|
29
26
|
|
|
30
|
-
export const configBarrelImport
|
|
27
|
+
export const configBarrelImport = check({
|
|
31
28
|
id: 'SK012',
|
|
32
29
|
blocking: true,
|
|
33
30
|
title: 'config import path',
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (!existsSync(p)) return void t.ok(`${CONFIG} absent — nothing to narrow`)
|
|
41
|
-
const src = readFileSync(p, 'utf8')
|
|
42
|
-
if (!BARREL.test(src)) return void t.ok(`${CONFIG} imports the narrow config module`)
|
|
31
|
+
description: 'repo.config.ts imports `defineRepo` from the narrow module, never the barrel',
|
|
32
|
+
owner: 'build/shipkit/config-keys.md',
|
|
33
|
+
run: (c) => {
|
|
34
|
+
const src = c.read(CONFIG)
|
|
35
|
+
if (src == null) return void c.ok(`${CONFIG} absent — nothing to narrow`)
|
|
36
|
+
if (!BARREL.test(src)) return void c.ok(`${CONFIG} imports the narrow config module`)
|
|
43
37
|
const line = src.split('\n').find((l) => /from\s+['"]rvlib-shipkit['"]/.test(l)) ?? ''
|
|
44
38
|
const symbols = importedSymbols(line)
|
|
45
39
|
const strays = symbols.filter((s) => !CONFIG_SYMBOLS.has(s))
|
|
46
40
|
if (strays.length > 0) {
|
|
47
|
-
|
|
48
|
-
return void
|
|
41
|
+
c.fail(`${CONFIG} imports ${strays.join(', ')} from the package barrel, which costs ~400ms of module load on every shipkit run`)
|
|
42
|
+
return void c.lines.push({ kind: 'fix-fail', text: `move ${strays.join(', ')} to their own module, then import from ${NARROW}` })
|
|
49
43
|
}
|
|
50
|
-
|
|
44
|
+
c.fail(`${CONFIG} imports from the package barrel (~400ms of module load on every shipkit run, the pre-commit hook included)`)
|
|
51
45
|
},
|
|
52
|
-
fix: (
|
|
53
|
-
const
|
|
54
|
-
if (
|
|
55
|
-
const src = readFileSync(p, 'utf8')
|
|
46
|
+
fix: (c): FixResult => {
|
|
47
|
+
const src = c.read(CONFIG)
|
|
48
|
+
if (src == null) return { changed: false, summary: `${CONFIG} absent` }
|
|
56
49
|
if (!BARREL.test(src)) return { changed: false, summary: 'already narrow' }
|
|
57
50
|
const line = src.split('\n').find((l) => /from\s+['"]rvlib-shipkit['"]/.test(l)) ?? ''
|
|
58
51
|
// only rewrite what the narrow module actually exports: silently
|
|
59
52
|
// repointing an import it cannot serve turns a slow gate into a broken one
|
|
60
53
|
if (importedSymbols(line).some((s) => !CONFIG_SYMBOLS.has(s))) return { changed: false, summary: 'imports symbols the narrow module does not export — move them by hand' }
|
|
61
|
-
|
|
54
|
+
c.write(CONFIG, src.replace(BARREL, `$1$2'${NARROW}'`))
|
|
62
55
|
return { changed: true, summary: `${CONFIG} now imports ${NARROW} (~400ms saved per run)` }
|
|
63
56
|
},
|
|
64
|
-
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
export default configBarrelImport
|