rvlib-shipkit 0.38.0 → 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 +371 -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 +216 -18
- 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
package/src/check.ts
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
// THE CHECK — one shape for everything a gate runs, and one way to add one.
|
|
2
|
+
//
|
|
3
|
+
// A repo drops `<name>.check.ts` anywhere, default-exports `check({...})`, and
|
|
4
|
+
// it shows up in `shipkit ci`, `shipkit check`, `shipkit checks` and the hub
|
|
5
|
+
// with no config edit. Whether it is a build step, a repo guard or a world
|
|
6
|
+
// probe is what the FIELDS say, never which registry it was put in. The
|
|
7
|
+
// toolkit's own built-ins are the same files under `src/checks/`, walked by
|
|
8
|
+
// the same code (core/discover-checks.ts).
|
|
9
|
+
//
|
|
10
|
+
// Two bodies, one object:
|
|
11
|
+
// `run` in process, ONE parameter `c` (core/check-ctx.ts): it reports
|
|
12
|
+
// through `c.ok/warn/fail` and reads the repo through `c.read`,
|
|
13
|
+
// `c.list`, `c.use(scan)`, which records what it read
|
|
14
|
+
// `cmd` argv, a subprocess, exit code is truth
|
|
15
|
+
//
|
|
16
|
+
// What a check declares about ITSELF is what every surface reads: `blocking`
|
|
17
|
+
// (can it stop you), how it is remembered (`ttl` or `never`, else a `run`
|
|
18
|
+
// check is keyed on what it read and a `cmd` check on its `inputs`), `when`
|
|
19
|
+
// (where it runs), `deps` (what must pass first). The file's own path joins
|
|
20
|
+
// its cache key automatically, so nothing declares its own code.
|
|
21
|
+
import type { CheckCtx } from './core/check-ctx.ts'
|
|
22
|
+
import { parseWhen } from './core/when.ts'
|
|
23
|
+
import type { Line } from './core/wire.ts'
|
|
24
|
+
import type { RepoConfig } from './repo-config.ts'
|
|
25
|
+
|
|
26
|
+
export { defineScan, mdPages, packageJson, type MdPage, type PackageJson, type Scan } from './scan.ts'
|
|
27
|
+
export type { CheckCtx, ScanCtx } from './core/check-ctx.ts'
|
|
28
|
+
|
|
29
|
+
/** where a check runs, for the hooks that run BEFORE its body (`cacheKey`) and
|
|
30
|
+
* for the legacy shapes. A body gets `c`, which carries both. */
|
|
31
|
+
export type CheckEnv = { root: string; config: RepoConfig }
|
|
32
|
+
|
|
33
|
+
/** whose check it is: the toolkit ships it, or this repo declared it */
|
|
34
|
+
export type CheckFrom = 'shipkit' | 'repo'
|
|
35
|
+
|
|
36
|
+
export type FixResult = { changed: boolean; summary: string }
|
|
37
|
+
|
|
38
|
+
export type CheckDef = {
|
|
39
|
+
/** the name every surface uses. Default: the file basename without
|
|
40
|
+
* `.check.ts`. A built-in carries `SK###`, stable forever, and a repo
|
|
41
|
+
* picks its own prefix or plain words. */
|
|
42
|
+
id?: string
|
|
43
|
+
/** display sugar, kept because `SK004` alone is unreadable on a row.
|
|
44
|
+
* Default: the id. */
|
|
45
|
+
title?: string
|
|
46
|
+
/** what MUST BE TRUE, in the present tense: ONE line, at most 90
|
|
47
|
+
* characters, as in `package.json routes lint, typecheck and test
|
|
48
|
+
* through the gate`. Never what red means, red is its negation. It
|
|
49
|
+
* renders at the top of the check page and as the tree row's tooltip.
|
|
50
|
+
* A title names the check, this states the invariant it holds. */
|
|
51
|
+
description: string
|
|
52
|
+
/** where the answer is OWNED: a wiki page for a built-in, the file itself
|
|
53
|
+
* for a repo check. It is the fix pointer floor whenever a failing body
|
|
54
|
+
* attached no precise pointer of its own: a file of this repo as it
|
|
55
|
+
* stands, a docs page after `docsBase`. Default: the file's
|
|
56
|
+
* repo-relative path. */
|
|
57
|
+
owner?: string
|
|
58
|
+
/** can this stop you? REQUIRED, no default. true = the failure is about
|
|
59
|
+
* THIS repo and blocks. false = it reports the WORLD (a box that is off,
|
|
60
|
+
* a cred nobody minted), so the failure prints as a warning and never
|
|
61
|
+
* blocks. The only question worth asking about a check, asked once. */
|
|
62
|
+
blocking: boolean
|
|
63
|
+
/** the body, in process. `c` is the one parameter: it reports (`c.ok`,
|
|
64
|
+
* `c.warn`, `c.fail`), says where (`c.root`, `c.config`, `c.frozen`),
|
|
65
|
+
* reads the repo with every read recorded (`c.read`, `c.json`,
|
|
66
|
+
* `c.exists`, `c.list`, `c.use(scan)`), and reaches the world (`c.exec`,
|
|
67
|
+
* `c.fetch`, `c.tool`). With no `inputs`, `ttl` or `never`, the answer is
|
|
68
|
+
* cached on what it read. One of `run` or `cmd`, never both. */
|
|
69
|
+
run?: (c: CheckCtx) => void | Promise<void>
|
|
70
|
+
/** a TOOL run as the body: argv without a shell, the repo root as the
|
|
71
|
+
* working directory (`bun run build`, `tsc`). The exit code is truth: a
|
|
72
|
+
* non-zero exit with no `FAIL:` line still fails loud. It never names the
|
|
73
|
+
* check file itself: a body of the file's own is `run` (SK020). */
|
|
74
|
+
cmd?: string[]
|
|
75
|
+
/** a BUILD STEP: `shipkit ci` runs it, the health pass never does. The
|
|
76
|
+
* gate asks "is my change good" and builds, `shipkit check` asks "is
|
|
77
|
+
* this repo healthy" and reports, and a tool chain has no business in
|
|
78
|
+
* the second question. A floor slot derives it, a file declares it, and
|
|
79
|
+
* it takes the 600s ceiling rather than the 120s one. `when` stays about
|
|
80
|
+
* TIME (always, release, health, off). A build step on `health` is an
|
|
81
|
+
* opt in one: the gate parks it and `shipkit ci --with <id>` runs it. */
|
|
82
|
+
build?: boolean
|
|
83
|
+
/** check ids that must pass first. One runner runs every check as one DAG,
|
|
84
|
+
* so a check naming none is a root of it, and a dep doubles as the race
|
|
85
|
+
* guard between two checks that touch one file. */
|
|
86
|
+
deps?: string[]
|
|
87
|
+
/** WHERE it runs: `always`, `release`, `health`, `off:<reason>`. The
|
|
88
|
+
* repo's `when: {}` map wins over it, so a config can move a check the
|
|
89
|
+
* repo does not own (core/when.ts owns the vocabulary). */
|
|
90
|
+
when?: string
|
|
91
|
+
/** CONTENT population: the repo-relative paths this check reads, so a
|
|
92
|
+
* cached answer is honest while they do not move. For a `cmd` check,
|
|
93
|
+
* whose subprocess reads what it likes. A `run` check that declares none
|
|
94
|
+
* of `inputs`, `ttl` and `never` is keyed on what it READ through `c`
|
|
95
|
+
* instead, the `reads` population (ledger/git-key.ts). A `cmd` check
|
|
96
|
+
* that declares none runs every time, which costs speed and never
|
|
97
|
+
* correctness. At most one of the three. */
|
|
98
|
+
inputs?: string[]
|
|
99
|
+
/** WORLD population: nothing on disk describes the answer, so a clean
|
|
100
|
+
* pass keeps for this long, as in '24h'. */
|
|
101
|
+
ttl?: string
|
|
102
|
+
/** a DECIDED never-cacheable, with the reason. Says somebody looked and
|
|
103
|
+
* no key can describe this, which takes it off the burn-down list. */
|
|
104
|
+
never?: string
|
|
105
|
+
/** it REWRITES the repo while it runs (updates a dependency, untracks a
|
|
106
|
+
* file) unless the run is frozen, so a frozen run asks it a different
|
|
107
|
+
* question: only a check declaring this carries `frozen` in its cache
|
|
108
|
+
* key, and every other check shares one answer between a frozen run and
|
|
109
|
+
* a normal one. Declared, never guessed from the body. */
|
|
110
|
+
writes?: boolean
|
|
111
|
+
/** cache-identity contribution for a `ttl` check whose verdict depends on
|
|
112
|
+
* config: a changed key busts the answer instantly instead of serving
|
|
113
|
+
* a stale cached assertion for the rest of the TTL. */
|
|
114
|
+
cacheKey?: (env: CheckEnv) => string
|
|
115
|
+
/** idempotent auto-repair, never interactive. `shipkit check --fix <id>`
|
|
116
|
+
* applies it, then re-runs the check. It gets the same `c` as the body,
|
|
117
|
+
* writes with `c.write` and `c.remove`, and is never cached. An `fs`
|
|
118
|
+
* import in the check file would keep an undeclared body off its reads
|
|
119
|
+
* (core/raw-access.ts). A repair that needs
|
|
120
|
+
* judgement stays a FAIL with a pointer. */
|
|
121
|
+
fix?: (c: CheckCtx) => FixResult | Promise<FixResult>
|
|
122
|
+
/** wall clock ceiling in ms. A `cmd` check is killed with its whole
|
|
123
|
+
* process tree at the ceiling. */
|
|
124
|
+
timeoutMs?: number
|
|
125
|
+
/** a label several checks share, so `--skip <group>` parks all of them
|
|
126
|
+
* and `--only <group>` runs all of them. */
|
|
127
|
+
group?: string
|
|
128
|
+
/** extra environment for a `cmd` check, merged over the run's own. */
|
|
129
|
+
env?: Record<string, string>
|
|
130
|
+
/** working directory for a `cmd` check (default: the repo root). */
|
|
131
|
+
cwd?: string
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** the longest description a row can show whole */
|
|
135
|
+
export const DESCRIPTION_MAX = 90
|
|
136
|
+
|
|
137
|
+
/** what is wrong with a description, or null when it states an invariant
|
|
138
|
+
* the way every surface expects: one line, short, the fact that must hold.
|
|
139
|
+
* A red-means clause is refused because red is always the negation. */
|
|
140
|
+
export function descriptionProblem(description: unknown): string | null {
|
|
141
|
+
if (typeof description !== 'string' || description.trim() === '')
|
|
142
|
+
return 'description is required: ONE line stating what must be true'
|
|
143
|
+
const d = description.trim()
|
|
144
|
+
if (/[\r\n]/.test(d)) return 'description must be ONE line'
|
|
145
|
+
if (d.length > DESCRIPTION_MAX) return `description is ${d.length} characters, the limit is ${DESCRIPTION_MAX}: state only what must be true`
|
|
146
|
+
if (/^this checks\b/i.test(d)) return `description states what must be true, with no 'this checks' opener`
|
|
147
|
+
if (/\b(red|yellow|green|a warning|warn)( one)? means\b|\ba red one\b/i.test(d))
|
|
148
|
+
return 'description states what must be true, never what red means (red is its negation)'
|
|
149
|
+
return null
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** the first candidate the shape accepts. A generated row states its
|
|
153
|
+
* invariant from what it knows, and falls back to a shorter spelling when a
|
|
154
|
+
* long path or argv would break the line limit */
|
|
155
|
+
export function describeFirst(...candidates: (string | null | undefined)[]): string {
|
|
156
|
+
for (const d of candidates) if (d != null && descriptionProblem(d) == null) return d.trim()
|
|
157
|
+
throw new Error(`no description candidate passes the shape: ${candidates.map((d) => JSON.stringify(d)).join(', ')}`)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** THE way a check file declares itself. It validates the shape at import
|
|
161
|
+
* time, so a malformed check dies when discovery loads it and never mid
|
|
162
|
+
* gate. The id, the title and the owner are filled in by discovery, which
|
|
163
|
+
* is the only thing that knows the file's name and place. */
|
|
164
|
+
export function check(d: CheckDef): CheckDef {
|
|
165
|
+
const has = (k: keyof CheckDef): boolean => d[k] != null
|
|
166
|
+
const name = d.id == null ? '' : ` ${d.id}`
|
|
167
|
+
const bad = descriptionProblem(d.description)
|
|
168
|
+
if (bad != null) throw new Error(`check${name}: ${bad}`)
|
|
169
|
+
if (typeof d.blocking !== 'boolean')
|
|
170
|
+
throw new Error(`check${d.id == null ? '' : ` ${d.id}`}: blocking is required — true (about this repo, it stops you) or false (about the world, it warns)`)
|
|
171
|
+
if (has('run') === has('cmd'))
|
|
172
|
+
throw new Error(`check${d.id == null ? '' : ` ${d.id}`}: declare exactly one body, \`run\` (in process) or \`cmd\` (argv)`)
|
|
173
|
+
const populations = (['inputs', 'ttl', 'never'] as const).filter((k) => has(k))
|
|
174
|
+
if (populations.length > 1)
|
|
175
|
+
throw new Error(`check${d.id == null ? '' : ` ${d.id}`}: ${populations.join(' and ')} contradict each other — a check reads files, or the world, or nothing can describe it`)
|
|
176
|
+
if (d.when != null) {
|
|
177
|
+
const w = parseWhen(d.when)
|
|
178
|
+
if (typeof w === 'string') throw new Error(`check${d.id == null ? '' : ` ${d.id}`}: when ${w}`)
|
|
179
|
+
}
|
|
180
|
+
return d
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── the legacy TASK shape, for the compatibility window ──────────────────
|
|
184
|
+
// A repo still declaring build steps through `repo.config.ts tasks: []`, and
|
|
185
|
+
// every repo decorating a floor slot through `defaults`, writes this one. It
|
|
186
|
+
// is read as a check whose body is its `cmd` or its `logic`, and it dies with
|
|
187
|
+
// those keys once `shipkit fleet` shows no repo below 0.40.
|
|
188
|
+
|
|
189
|
+
export type TaskDef = {
|
|
190
|
+
/** the name every surface uses: the gate line, `--only`, `--skip`, a
|
|
191
|
+
* `deps` entry, a `when` key and the ledger row. Unique in the repo. */
|
|
192
|
+
id: string
|
|
193
|
+
/** one line stating what must be true (the shape: descriptionProblem).
|
|
194
|
+
* A floor slot and a unit test check get theirs from the
|
|
195
|
+
* generator, a task that names none reads as its command exiting 0 */
|
|
196
|
+
description?: string
|
|
197
|
+
/** argv, run without a shell. One of `cmd` or `logic` per task. */
|
|
198
|
+
cmd?: string[]
|
|
199
|
+
/** the body as a function, for work that is already TypeScript. It gets
|
|
200
|
+
* the wire context, so it reports `ok:` / `WARN` / `FAIL:` lines the way
|
|
201
|
+
* a subprocess does. The legacy spelling of a check's `run`. */
|
|
202
|
+
logic?: (t: CheckCtx) => unknown
|
|
203
|
+
/** ids that must pass first. The DAG runs everything else in parallel, so
|
|
204
|
+
* a dep doubles as the race guard between two that touch one file. */
|
|
205
|
+
deps?: string[]
|
|
206
|
+
/** extra environment for a `cmd` task, merged over the run's own */
|
|
207
|
+
env?: Record<string, string>
|
|
208
|
+
/** working directory for a `cmd` task (default: the repo root) */
|
|
209
|
+
cwd?: string
|
|
210
|
+
/** a label several tasks share, so `--skip <group>` parks all of them at
|
|
211
|
+
* once (a local bench, a slow integration tier). */
|
|
212
|
+
group?: string
|
|
213
|
+
/** DEPRECATED, read into `when` as `health`: out of the gate until
|
|
214
|
+
* `--with <id>` names it. */
|
|
215
|
+
optIn?: boolean
|
|
216
|
+
/** wall clock ceiling in ms, default 600000 for a build step. One that
|
|
217
|
+
* hits it is killed with its whole process tree, because the cmd is
|
|
218
|
+
* usually a wrapper. */
|
|
219
|
+
timeoutMs?: number
|
|
220
|
+
/** CONTENT population: the repo-relative paths this task reads. Declaring
|
|
221
|
+
* them is what makes it keyable (ledger/git-key.ts). Undeclared is the
|
|
222
|
+
* DEFAULT and means never cached, so forgetting one costs speed and
|
|
223
|
+
* never correctness. A list MISSING a path the command reads is the one
|
|
224
|
+
* that costs correctness: the key cannot move when that path changes, so
|
|
225
|
+
* a cached answer comes back over a tree it no longer describes. `['.']` is the
|
|
226
|
+
* whole TRACKED tree, and it is NOT a catch-all: an ignored path enters a
|
|
227
|
+
* key by name and never by content, so a task reading a gitignored
|
|
228
|
+
* fixture, a built dir or a .env under declares with `['.']` exactly as
|
|
229
|
+
* it would with a narrow list. */
|
|
230
|
+
inputs?: string[]
|
|
231
|
+
/** WORLD population: it reads the network, a keychain, $PATH or the clock,
|
|
232
|
+
* so nothing on disk describes its answer and a clean pass is remembered
|
|
233
|
+
* for this long, as in '24h'. */
|
|
234
|
+
ttl?: string
|
|
235
|
+
/** a DECIDED never-cacheable, with the reason. */
|
|
236
|
+
never?: string
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** every field a `TaskDef` may carry. Declared as a value so `defineRepo` can
|
|
240
|
+
* refuse a typo inside a `defaults` slot: a slot with no `cmd` DECORATES the
|
|
241
|
+
* resolved task, so a misspelled field is not a type error at the call site
|
|
242
|
+
* and would land as a task that is quietly never cached. Add a field above,
|
|
243
|
+
* add it here. */
|
|
244
|
+
export const TASK_DEF_KEYS = ['id', 'cmd', 'logic', 'deps', 'env', 'cwd', 'group', 'optIn', 'timeoutMs', 'inputs', 'ttl', 'never'] as const
|
|
245
|
+
|
|
246
|
+
// ── the legacy rule shape, for the compatibility window ──────────────────
|
|
247
|
+
// A repo still registering through `repo.config.ts rules: []` writes this
|
|
248
|
+
// one. It is read as a check whose body is `lint`, and it dies with that key
|
|
249
|
+
// once `shipkit fleet` shows no repo below 0.40.
|
|
250
|
+
|
|
251
|
+
export type RuleEnv = CheckEnv
|
|
252
|
+
|
|
253
|
+
export type Rule = {
|
|
254
|
+
/** stable PREFIX + 3 digits, never reused or renumbered. `SK###` is
|
|
255
|
+
* reserved for the toolkit, so a repo picks its own (`RV###`). A check
|
|
256
|
+
* FILE takes its id from its name and may use plain words instead. */
|
|
257
|
+
id: string
|
|
258
|
+
/** display name, stable: the row in STATUS.md, the TUI, the suite cache */
|
|
259
|
+
title: string
|
|
260
|
+
/** can this stop you? REQUIRED, no default, exactly as a check declares it */
|
|
261
|
+
blocking: boolean
|
|
262
|
+
/** wiki page owning the invariant — the body runner appends
|
|
263
|
+
* `docsBase + owner` as the fix: pointer whenever a failing lint
|
|
264
|
+
* attached none. A check file defaults this to its own path. */
|
|
265
|
+
owner: string
|
|
266
|
+
/** WORLD population: clean-pass cache duration for a rule that probes the
|
|
267
|
+
* network, as in '24h'. A skip, a WARN and a FAIL are never cached. */
|
|
268
|
+
ttl?: string
|
|
269
|
+
/** CONTENT population: the repo-relative paths this rule reads. Neither
|
|
270
|
+
* `inputs` nor `ttl` means never cached, which is the safe default, and
|
|
271
|
+
* declaring both is a registration failure. */
|
|
272
|
+
inputs?: string[]
|
|
273
|
+
/** repo-relative entry file(s) of this rule's OWN code, so fixing a bug in
|
|
274
|
+
* the rule moves its key. A check FILE needs none: its own path is its
|
|
275
|
+
* closure, which is the field this shape exists without. */
|
|
276
|
+
code?: string[]
|
|
277
|
+
/** a DECIDED never-cacheable, with the reason: it reads git history, a
|
|
278
|
+
* path outside the repo, or the wall clock, so no key can describe it. */
|
|
279
|
+
never?: string
|
|
280
|
+
/** cache-identity contribution for a TTL'd rule whose verdict depends on
|
|
281
|
+
* config: a changed key busts the answer instead of serving it cached */
|
|
282
|
+
cacheKey?: (env: RuleEnv) => string
|
|
283
|
+
/** the invariant itself, the legacy spelling of a check's `run` */
|
|
284
|
+
lint: (t: CheckCtx, env: RuleEnv) => void | Promise<void>
|
|
285
|
+
/** idempotent auto-repair, never interactive. `check --fix` applies it */
|
|
286
|
+
fix?: (env: RuleEnv) => FixResult | Promise<FixResult>
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ── running a body, and what every line it prints carries ────────────────
|
|
290
|
+
|
|
291
|
+
/** the config part of a check's cache identity. Where it RUNS is part of it:
|
|
292
|
+
* moving one off the gate, or editing the reason it is off, has to ask the
|
|
293
|
+
* question again rather than serve the cached answer from before the move. */
|
|
294
|
+
export function checkCacheExtra(u: { id: string; cacheKey?: (env: CheckEnv) => string }, env: CheckEnv, at: { at: string; reason: string | null; source: string }): string | undefined {
|
|
295
|
+
const key = u.cacheKey?.(env)
|
|
296
|
+
if (at.source === 'default') return key
|
|
297
|
+
return at.at === 'off' ? `${key ?? ''}|exempt:${at.reason ?? ''}` : `${key ?? ''}|when:${at.at}`
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** THE one way to run an in-process body (the check suite and the `--fix`
|
|
301
|
+
* runner both come through here): a throwing body still fails with its id,
|
|
302
|
+
* a failing or warning run always carries at least one `fix:` pointer (the
|
|
303
|
+
* body's own precise ones win, else `docsBase` plus the check's owner), and
|
|
304
|
+
* every line gets tagged. */
|
|
305
|
+
export async function runCheckBody(
|
|
306
|
+
u: { id: string; title: string; owner: string; run?: CheckDef['run']; fix?: unknown },
|
|
307
|
+
c: CheckCtx,
|
|
308
|
+
/** the check is OFF: it still enters the suite, so the reason stays
|
|
309
|
+
* visible in every output instead of the row disappearing from the list */
|
|
310
|
+
off?: { reason: string | null },
|
|
311
|
+
): Promise<void> {
|
|
312
|
+
if (off != null) {
|
|
313
|
+
c.lines.push({ kind: 'skip', text: `${u.id} exempt: ${off.reason ?? ''}` })
|
|
314
|
+
c.lines = tagCheckLines(u, c.lines)
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
await u.run?.(c)
|
|
319
|
+
} catch (e) {
|
|
320
|
+
// a 429 is no answer at all, so it warns and nothing remembers it
|
|
321
|
+
if (e instanceof Error && e.name === 'RateLimited') c.unreachable(`${u.title} not checked this run (${e.message}), retry later`)
|
|
322
|
+
else c.fail(`${u.title} body errored (${String(e).slice(0, 200)}) — network down, or a bug in this check`)
|
|
323
|
+
}
|
|
324
|
+
// the pass's config, never `c.config`: the pointer is the runner's line,
|
|
325
|
+
// and it must not enter the body's read set
|
|
326
|
+
if ((c.failed || c.warned) && !c.lines.some((l) => l.kind === 'fix-fail' || l.kind === 'fix-warn'))
|
|
327
|
+
c.lines.push({ kind: c.failed ? 'fix-fail' : 'fix-warn', text: c.ownerPointer(u.owner) })
|
|
328
|
+
c.lines = tagCheckLines(u, c.lines)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** tag every severity line with the check id; a fix pointer of a fixable check
|
|
332
|
+
* advertises the autofix. Idempotent, since an already-tagged line passes
|
|
333
|
+
* back through here on a cached suite replay. */
|
|
334
|
+
export function tagCheckLines(u: { id: string; fix?: unknown }, lines: Line[]): Line[] {
|
|
335
|
+
return lines.map((l) => {
|
|
336
|
+
if (l.kind === 'fix-warn' || l.kind === 'fix-fail')
|
|
337
|
+
return u.fix == null || l.text.includes('--fix ')
|
|
338
|
+
? l
|
|
339
|
+
: { ...l, text: `${l.text} · autofix: shipkit check --fix ${u.id}` }
|
|
340
|
+
if (l.kind === 'note') return l
|
|
341
|
+
return l.text.startsWith(`[${u.id}]`) ? l : { ...l, text: `[${u.id}] ${l.text}` }
|
|
342
|
+
})
|
|
343
|
+
}
|
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
// SK001 — canonical package.json shape (meta/rv-conventions/package-json.md):
|
|
2
2
|
// private:true, no license, capability-required script names FAIL, banned
|
|
3
3
|
// aliases + missing baseline scripts WARN. Blocking, so it runs in `shipkit
|
|
4
|
-
// ci` like every other rule.
|
|
4
|
+
// ci` like every other rule. It reads only through `c`, so it is cached on
|
|
5
|
+
// what it read: package.json, the probes below, the test file listing, and
|
|
6
|
+
// whatever the floor resolution read.
|
|
5
7
|
//
|
|
6
8
|
// It also enforces the package.json half of ONE WAY IN (build/shipkit.md):
|
|
7
9
|
// `test`, `lint` and `typecheck` ROUTE to `shipkit ci --only <id>` and the
|
|
8
10
|
// tool body they used to hold sits at `<id>:raw`. That is what makes a cache
|
|
9
11
|
// worth having: `bun run test` is the reflex, and the reflex has to land in
|
|
10
12
|
// shipkit rather than beside it.
|
|
11
|
-
import { existsSync, readdirSync, readFileSync } from 'fs'
|
|
12
|
-
import { join } from 'path'
|
|
13
13
|
import { canonicalBodies, conditionalBodies, inferBin, ROUTED_SCRIPTS, rawName, routedBody, routesToShipkit } from '../core/canonical-scripts.ts'
|
|
14
14
|
import type { CheckCtx } from '../core/check-ctx.ts'
|
|
15
|
-
import { resolveFloor } from '../core/floor.ts'
|
|
15
|
+
import { checkFloorReader, resolveFloor } from '../core/floor-resolve.ts'
|
|
16
16
|
import type { RepoConfig } from '../repo-config.ts'
|
|
17
|
-
import
|
|
18
|
-
import { atomicWrite } from '../core/locked-write.ts'
|
|
17
|
+
import { check, type FixResult } from '../check.ts'
|
|
19
18
|
|
|
20
19
|
type Pkg = { name?: string; private?: boolean; license?: string; scripts?: Record<string, string> }
|
|
21
20
|
|
|
@@ -35,10 +34,13 @@ const BASELINE = ['test', 'lint', 'fmt'] as const
|
|
|
35
34
|
// no per-line fix pointers here — the rule framework appends ONE pointer
|
|
36
35
|
// (docsBase + OWNER) to a failing/warning run, the same one-per-severity
|
|
37
36
|
// doctrine the contracts engine follows
|
|
38
|
-
export
|
|
37
|
+
export function evaluatePackageJson(t: CheckCtx): void {
|
|
38
|
+
const c = t.config
|
|
39
39
|
let pkg: Pkg
|
|
40
40
|
try {
|
|
41
|
-
|
|
41
|
+
const read = t.json<Pkg>('package.json')
|
|
42
|
+
if (read == null) throw new Error('no package.json')
|
|
43
|
+
pkg = read
|
|
42
44
|
} catch (e) {
|
|
43
45
|
t.fail(`package.json unreadable (${String(e).slice(0, 120)})`)
|
|
44
46
|
return
|
|
@@ -72,7 +74,7 @@ export async function evaluatePackageJson(t: CheckCtx, root: string, c: RepoConf
|
|
|
72
74
|
|
|
73
75
|
const has = (name: string): boolean => scripts[name] != null || aliased.has(name)
|
|
74
76
|
|
|
75
|
-
const required = requiredScripts(
|
|
77
|
+
const required = requiredScripts(t)
|
|
76
78
|
const missing = required.filter((r) => !has(r.name))
|
|
77
79
|
for (const m of missing) t.fail(`package.json: missing script '${m.name}' (${m.why})`)
|
|
78
80
|
|
|
@@ -80,7 +82,7 @@ export async function evaluatePackageJson(t: CheckCtx, root: string, c: RepoConf
|
|
|
80
82
|
// reason a cache gets bypassed: an agent types `bun run test` and shipkit
|
|
81
83
|
// never hears about it. A slot the repo turned off has no shipkit task to
|
|
82
84
|
// route to, so it is not governed.
|
|
83
|
-
const floor = resolveFloor(c, root)
|
|
85
|
+
const floor = resolveFloor(c, t.root, checkFloorReader(t))
|
|
84
86
|
for (const id of ROUTED_SCRIPTS) {
|
|
85
87
|
if (routedExempt(c).has(id) || c.defaults?.[id] === false) continue
|
|
86
88
|
const body = scripts[id]
|
|
@@ -90,13 +92,13 @@ export async function evaluatePackageJson(t: CheckCtx, root: string, c: RepoConf
|
|
|
90
92
|
continue
|
|
91
93
|
}
|
|
92
94
|
const gap = floor.unfilled.find((u) => u.id === id)
|
|
93
|
-
if (gap != null) t.fail(`package.json: ${gap.reason} — \`bun run ${id}\` would
|
|
95
|
+
if (gap != null) t.fail(`package.json: ${gap.reason} — \`bun run ${id}\` would run nothing`)
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
// `test` is asked for only once the repo HAS tests: the fixer deliberately
|
|
97
99
|
// refuses to scaffold it before that (a fixer never manufactures a
|
|
98
100
|
// failure), so nagging for it would be a WARN no machine may clear
|
|
99
|
-
const baseline = BASELINE.filter((n) => n !== 'test' || hasTestFiles(
|
|
101
|
+
const baseline = BASELINE.filter((n) => n !== 'test' || hasTestFiles(testFiles(t)))
|
|
100
102
|
const missingBaseline = baseline.filter((n) => !has(n))
|
|
101
103
|
if (missingBaseline.length > 0)
|
|
102
104
|
t.warn(`package.json: missing baseline script(s): ${missingBaseline.join(', ')}`)
|
|
@@ -111,13 +113,13 @@ export async function evaluatePackageJson(t: CheckCtx, root: string, c: RepoConf
|
|
|
111
113
|
// a shipkit verb is never on this list. `shipkit ci`, `check`, `contracts`,
|
|
112
114
|
// `tui`, `mv`, `assets`, `asks` are typed as themselves, and a package.json
|
|
113
115
|
// row that only forwards to one is a second name for one capability
|
|
114
|
-
function requiredScripts(
|
|
116
|
+
function requiredScripts(c: CheckCtx): Array<{ name: string; why: string }> {
|
|
115
117
|
const required: Array<{ name: string; why: string }> = [
|
|
116
118
|
// watch-mode serve — the ONE shipkit surface the bare verb cannot give
|
|
117
119
|
{ name: 'hub', why: 'shipkit repo' },
|
|
118
120
|
]
|
|
119
|
-
if (
|
|
120
|
-
if (
|
|
121
|
+
if (c.exists('.shipkit/comfy')) required.push({ name: 'comfy:up', why: '.shipkit/comfy/ exists' })
|
|
122
|
+
if (c.exists('deploy.sh')) required.push({ name: 'up', why: 'deploy.sh exists' })
|
|
121
123
|
return required
|
|
122
124
|
}
|
|
123
125
|
|
|
@@ -145,42 +147,36 @@ function routedExempt(c: RepoConfig): Set<string> {
|
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
const TEST_SKIP = new Set(['node_modules', 'dist', 'tmp', 'references', 'coverage'])
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
return
|
|
161
|
-
}
|
|
162
|
-
return walk(root, 0)
|
|
150
|
+
const TEST_FILE = /\.(test|spec)\.[cm]?[jt]sx?$/
|
|
151
|
+
|
|
152
|
+
/** the repo's test file candidates as git sees them, one recorded listing:
|
|
153
|
+
* a new test file reruns the check, an edit anywhere else does not */
|
|
154
|
+
const testFiles = (c: CheckCtx): string[] => c.list('**/*.{test,spec}.*')
|
|
155
|
+
|
|
156
|
+
/** any `*.test.ts` among these repo-relative paths, outside installs, build
|
|
157
|
+
* output and dot dirs, so the test script is only scaffolded once it has
|
|
158
|
+
* something to run */
|
|
159
|
+
export function hasTestFiles(paths: readonly string[]): boolean {
|
|
160
|
+
return paths.some((f) => {
|
|
161
|
+
const parts = f.split('/')
|
|
162
|
+
return TEST_FILE.test(parts.at(-1)!) && parts.slice(0, -1).every((d) => !TEST_SKIP.has(d) && !d.startsWith('.'))
|
|
163
|
+
})
|
|
163
164
|
}
|
|
164
165
|
|
|
165
|
-
export const packageJsonShape
|
|
166
|
+
export const packageJsonShape = check({
|
|
166
167
|
id: 'SK001',
|
|
167
168
|
blocking: true,
|
|
168
169
|
title: 'package.json',
|
|
170
|
+
description: 'package.json routes lint, typecheck and test through the gate',
|
|
169
171
|
owner: OWNER,
|
|
170
|
-
|
|
171
|
-
// check also walks the repo for a `*.test.ts` and probes `.rv-comfy`
|
|
172
|
-
// and `deploy.sh`, so naming three files would key it on less than it
|
|
173
|
-
// reads
|
|
174
|
-
inputs: ['.'],
|
|
175
|
-
lint: (t: CheckCtx, env: RuleEnv) => evaluatePackageJson(t, env.root, env.config),
|
|
172
|
+
run: (c) => evaluatePackageJson(c),
|
|
176
173
|
// mechanical repairs only: field flips + scripts whose canonical body the
|
|
177
174
|
// vocabulary page states. Bodies it does NOT state (assets, comfy:up) are
|
|
178
175
|
// named in the summary, never invented. rvlib-* roots untouched — their
|
|
179
176
|
// license/private are publish decisions.
|
|
180
|
-
fix: (
|
|
181
|
-
const
|
|
182
|
-
if (
|
|
183
|
-
const raw = readFileSync(pkgPath, 'utf8')
|
|
177
|
+
fix: (c): FixResult => {
|
|
178
|
+
const raw = c.read('package.json')
|
|
179
|
+
if (raw == null) return { changed: false, summary: 'no package.json — nothing to fix' }
|
|
184
180
|
let pkg: Pkg & { scripts?: Record<string, string> }
|
|
185
181
|
try { pkg = JSON.parse(raw) as Pkg } catch { return { changed: false, summary: 'package.json unparseable — repair it by hand first' } }
|
|
186
182
|
if (pkg.name?.startsWith('rvlib-') === true) return { changed: false, summary: 'rvlib-* package — license/private are publish decisions, not autofixable' }
|
|
@@ -207,7 +203,7 @@ export const packageJsonShape: Rule = {
|
|
|
207
203
|
const aliasSatisfied = new Set<string>()
|
|
208
204
|
for (const [name, a] of Object.entries(ALIASES))
|
|
209
205
|
if (pkg.scripts[name] != null && a.bodyMatch.test(pkg.scripts[name]!)) aliasSatisfied.add(a.canonical)
|
|
210
|
-
for (const m of requiredScripts(
|
|
206
|
+
for (const m of requiredScripts(c).filter((r) => pkg.scripts![r.name] == null && !aliasSatisfied.has(r.name))) {
|
|
211
207
|
const body = bodies[m.name]
|
|
212
208
|
if (body == null) { skipped.push(m.name); continue }
|
|
213
209
|
pkg.scripts[m.name] = body
|
|
@@ -216,13 +212,13 @@ export const packageJsonShape: Rule = {
|
|
|
216
212
|
// only once the repo HAS tests. `bun test` exits non-zero when it finds
|
|
217
213
|
// none, so scaffolding this into a repo without any turns the gate red
|
|
218
214
|
// on work the fixer itself did. a fixer must never manufacture a failure.
|
|
219
|
-
if (pkg.scripts['test'] == null && hasTestFiles(
|
|
215
|
+
if (pkg.scripts['test'] == null && hasTestFiles(testFiles(c))) { pkg.scripts['test'] = 'bun test'; changes.push(`script 'test' added`) }
|
|
220
216
|
// ONE WAY IN, last so it also routes whatever this pass just added:
|
|
221
217
|
// the tool body slides to `<id>:raw` and `<id>` becomes the shipkit
|
|
222
218
|
// call. Both keys keep their place in the file, so the diff a human
|
|
223
219
|
// reads is the two lines that changed and not a reordered block
|
|
224
220
|
for (const id of ROUTED_SCRIPTS) {
|
|
225
|
-
if (routedExempt(
|
|
221
|
+
if (routedExempt(c.config).has(id) || c.config.defaults?.[id] === false) continue
|
|
226
222
|
const body = pkg.scripts[id]
|
|
227
223
|
if (body == null || routesToShipkit(body)) continue
|
|
228
224
|
const raw = rawName(id)
|
|
@@ -236,7 +232,7 @@ export const packageJsonShape: Rule = {
|
|
|
236
232
|
}
|
|
237
233
|
if (changes.length > 0) {
|
|
238
234
|
const indent = raw.match(/^(\s+)"/m)?.[1] ?? ' '
|
|
239
|
-
|
|
235
|
+
c.write('package.json', JSON.stringify(pkg, null, indent) + '\n')
|
|
240
236
|
}
|
|
241
237
|
const summary = [
|
|
242
238
|
changes.length > 0 ? changes.join(', ') : 'nothing mechanical to change',
|
|
@@ -245,4 +241,6 @@ export const packageJsonShape: Rule = {
|
|
|
245
241
|
].join(' · ')
|
|
246
242
|
return { changed: changes.length > 0, summary }
|
|
247
243
|
},
|
|
248
|
-
}
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
export default packageJsonShape
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// SK002 — .shipkit/* contract fulfillness (meta/contracts.md). The rule is the
|
|
2
|
+
// registration point; the evaluation machinery stays in
|
|
3
|
+
// contracts/contracts-engine.ts (venue manifests are DATA rows, not rules —
|
|
4
|
+
// one SK id covers the invariant "declared contracts are fulfilled, folders
|
|
5
|
+
// can't rot undeclared"). Every surface reporting fulfillness (`check`,
|
|
6
|
+
// `shipkit contracts`, the ci `contracts` check) tags with this id via
|
|
7
|
+
// contracts/apply-contracts.ts.
|
|
8
|
+
import { applyContractsResult, contractsResult } from '../contracts/apply-contracts.ts'
|
|
9
|
+
import { check } from '../check.ts'
|
|
10
|
+
|
|
11
|
+
export const contractsFulfillness = check({
|
|
12
|
+
id: 'SK002',
|
|
13
|
+
blocking: true,
|
|
14
|
+
title: 'contracts',
|
|
15
|
+
description: 'every declared venue has a complete contract folder',
|
|
16
|
+
owner: 'meta/contracts.md',
|
|
17
|
+
// keyed on what the contracts read: each artifact, each detector, and each
|
|
18
|
+
// folder a glob walked through `c.entries`, a gitignored one included
|
|
19
|
+
run: async (c) => {
|
|
20
|
+
applyContractsResult(c, await c.use(contractsResult), contractsFulfillness)
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export default contractsFulfillness
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// SK003 — the CLAUDE.md two-line contract (meta/agent-folder.md): CLAUDE.md
|
|
2
|
+
// is an include index, never a doc; `@./agent/index.md` comes LAST (include
|
|
3
|
+
// order is SEMANTIC — base context first, repo docs after so they
|
|
4
|
+
// override); a
|
|
5
|
+
// public repo's `@./CLAUDE.local.md` line-1 indirection must be gitignored.
|
|
6
|
+
// The rule asserts no base include path of its own: `shipkit start` owns
|
|
7
|
+
// which one lands (its house template, or `--include`). A repo with neither
|
|
8
|
+
// CLAUDE.md nor agent/ is not a two-line-contract repo and passes.
|
|
9
|
+
import { check, type FixResult } from '../check.ts'
|
|
10
|
+
|
|
11
|
+
/** the repo router line, always LAST: start writes it whenever agent/index.md exists */
|
|
12
|
+
export const AGENT_LINE = '@./agent/index.md'
|
|
13
|
+
const LOCAL_LINE = '@./CLAUDE.local.md'
|
|
14
|
+
|
|
15
|
+
export const claudeMdShape = check({
|
|
16
|
+
id: 'SK003',
|
|
17
|
+
blocking: true,
|
|
18
|
+
title: 'CLAUDE.md',
|
|
19
|
+
description: 'CLAUDE.md loads the house wiki into every session',
|
|
20
|
+
owner: 'meta/agent-folder.md',
|
|
21
|
+
// keyed on what it reads: CLAUDE.md and whether agent/index.md exists. The
|
|
22
|
+
// gitignore probe runs git, so an answer that took it is never stored
|
|
23
|
+
run: async (c) => {
|
|
24
|
+
const hasAgent = c.exists('agent/index.md')
|
|
25
|
+
const claude = c.read('CLAUDE.md')
|
|
26
|
+
if (claude == null) {
|
|
27
|
+
if (hasAgent) c.fail(`agent/index.md exists but there is no CLAUDE.md routing it — the two-line contract wants ${AGENT_LINE}`)
|
|
28
|
+
else c.ok('no CLAUDE.md / agent — not a two-line-contract repo')
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
const lines = claude.split('\n').filter((l) => l.trim() !== '')
|
|
32
|
+
const includes = lines.filter((l) => l.startsWith('@'))
|
|
33
|
+
const prose = lines.length - includes.length
|
|
34
|
+
if (prose > 0)
|
|
35
|
+
c.fail(`CLAUDE.md carries ${prose} non-include line(s) — it is a tiny include index, content belongs in agent/index.md`)
|
|
36
|
+
if (includes.length > 2)
|
|
37
|
+
c.fail(`CLAUDE.md has ${includes.length} include lines — the contract is 2: base context, then ${AGENT_LINE}`)
|
|
38
|
+
if (hasAgent && !lines.includes(AGENT_LINE))
|
|
39
|
+
c.fail(`agent/index.md exists but CLAUDE.md does not import it — add ${AGENT_LINE} as the LAST line`)
|
|
40
|
+
if (lines.includes(AGENT_LINE) && lines.at(-1) !== AGENT_LINE)
|
|
41
|
+
c.fail(`include order inverted — base context first, ${AGENT_LINE} LAST (order is semantic: repo docs override the base)`)
|
|
42
|
+
if (lines.includes(LOCAL_LINE)) {
|
|
43
|
+
const r = await c.exec(['git', 'check-ignore', '-q', 'CLAUDE.local.md'], { cwd: c.root, timeoutMs: 5_000 })
|
|
44
|
+
if (r.code === 1)
|
|
45
|
+
c.fail(`${LOCAL_LINE} is imported but CLAUDE.local.md is not gitignored — the machine-local include must never be tracked`)
|
|
46
|
+
else if (r.code !== 0)
|
|
47
|
+
c.note(`CLAUDE.local.md gitignore not verifiable (git says: ${r.out.trim().split('\n')[0] ?? `exit ${r.code}`})`)
|
|
48
|
+
}
|
|
49
|
+
if (!c.failed) c.ok(`CLAUDE.md include-index ok (${lines.length} line(s))`)
|
|
50
|
+
},
|
|
51
|
+
// the one mechanical repair: the missing agent/index.md route, appended
|
|
52
|
+
// to an EXISTING CLAUDE.md. Never creates the file — a created one-line
|
|
53
|
+
// CLAUDE.md would lint green with the base include forever missing,
|
|
54
|
+
// trading a loud FAIL for a permanent silent gap. Creation and prose
|
|
55
|
+
// relocation stay human/machine-local (`shipkit start --include`).
|
|
56
|
+
fix: (c): FixResult => {
|
|
57
|
+
if (!c.exists('agent/index.md')) return { changed: false, summary: 'no agent/index.md — nothing mechanical to fix' }
|
|
58
|
+
const cur = c.read('CLAUDE.md')
|
|
59
|
+
if (cur == null) return { changed: false, summary: 'CLAUDE.md missing — creation needs the base include (shipkit start <name> writes it), not autofixable' }
|
|
60
|
+
if (cur.split('\n').includes(AGENT_LINE)) return { changed: false, summary: `${AGENT_LINE} already routed — remaining findings need a human` }
|
|
61
|
+
c.write('CLAUDE.md', `${cur.endsWith('\n') || cur === '' ? cur : `${cur}\n`}${AGENT_LINE}\n`)
|
|
62
|
+
return { changed: true, summary: `${AGENT_LINE} appended as the last line` }
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
export default claudeMdShape
|