rvlib-shipkit 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +105 -0
- package/README.md +17 -17
- package/package.json +3 -2
- package/src/asks/asks-queue.ts +4 -2
- package/src/assets/kits/identity-kit.ts +15 -13
- package/src/assets/run-assets.ts +12 -2
- package/src/check.ts +73 -98
- package/src/checks/SK001-package-json-shape.check.ts +26 -4
- package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
- package/src/checks/SK003-claude-md-shape.check.ts +5 -1
- package/src/checks/SK004-gitignore-policy.check.ts +7 -4
- package/src/checks/SK005-rvlib-versions.check.ts +53 -21
- package/src/checks/SK006-npm-publish-safety.check.ts +4 -0
- package/src/checks/SK007-github-visibility.check.ts +4 -2
- package/src/checks/SK008-reviewer-agent.check.ts +6 -4
- package/src/checks/SK009-commit-ai-tells.check.ts +94 -74
- package/src/checks/SK010-undeclared-unit.check.ts +4 -0
- package/src/checks/SK011-sibling-unit-ranges.check.ts +4 -0
- package/src/checks/SK012-config-barrel-import.check.ts +5 -1
- package/src/checks/SK013-one-way-in-policy.check.ts +7 -3
- package/src/checks/SK014-prose-ste.check.ts +20 -6
- package/src/checks/SK015-nested-repo.check.ts +4 -2
- package/src/checks/SK016-legacy-check-shapes.check.ts +23 -16
- package/src/checks/SK017-vscode-hub-task.check.ts +22 -7
- package/src/checks/SK018-test-files-run.check.ts +6 -2
- package/src/checks/SK019-tests-run-in-units.check.ts +8 -4
- package/src/checks/SK020-self-spawning-check.check.ts +4 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -4
- package/src/checks/SK024-identity-colors.check.ts +5 -3
- package/src/checks/SK025-release-drift.check.ts +4 -5
- package/src/checks/SK026-asks-autoclose.check.ts +4 -4
- package/src/checks/SK027-agent-wiki-links.check.ts +34 -9
- package/src/checks/SK028-readme.check.ts +10 -2
- package/src/checks/SK029-explicit-check-files.check.ts +95 -0
- package/src/cli/cmd_Asks.ts +5 -5
- package/src/cli/cmd_Assets.ts +7 -7
- package/src/cli/cmd_Bump.ts +12 -11
- package/src/cli/cmd_Check.ts +8 -7
- package/src/cli/cmd_Checks.ts +7 -6
- package/src/cli/cmd_Ci.ts +12 -11
- package/src/cli/cmd_Commands.ts +16 -13
- package/src/cli/cmd_ConfigKeys.ts +7 -6
- package/src/cli/cmd_Contracts.ts +3 -3
- package/src/cli/cmd_CrossRepoActivity.ts +10 -6
- package/src/cli/cmd_Fleet.ts +9 -7
- package/src/cli/cmd_GlobalProcesses.ts +116 -4
- package/src/cli/cmd_Home.ts +23 -17
- package/src/cli/cmd_Hooks.ts +6 -5
- package/src/cli/cmd_Init.ts +5 -4
- package/src/cli/cmd_Keys.ts +8 -7
- package/src/cli/cmd_Mv.ts +7 -6
- package/src/cli/cmd_Needs.ts +4 -3
- package/src/cli/cmd_Overview.ts +4 -3
- package/src/cli/cmd_Pb.ts +2 -2
- package/src/cli/cmd_Planned.ts +6 -5
- package/src/cli/cmd_Prose.ts +5 -5
- package/src/cli/cmd_Reflect.ts +7 -7
- package/src/cli/cmd_Release.ts +6 -5
- package/src/cli/cmd_Releases.ts +5 -4
- package/src/cli/cmd_Serve.ts +4 -3
- package/src/cli/cmd_Smoke.ts +5 -4
- package/src/cli/cmd_Start.ts +7 -6
- package/src/cli/cmd_Status.ts +4 -3
- package/src/cli/cmd_Tui.ts +3 -2
- package/src/cli/cmd_When.ts +71 -0
- package/src/cli/cmd_Wiki.ts +4 -4
- package/src/cli.ts +7 -2
- package/src/collections/checks.ts +2 -0
- package/src/collections/collection-types.ts +24 -4
- package/src/collections/providers.ts +6 -2
- package/src/contracts/apply-contracts.ts +9 -8
- package/src/contracts/contract-types.ts +8 -2
- package/src/contracts/contracts-engine.ts +34 -13
- package/src/contracts/manifests/identity.contract.ts +6 -5
- package/src/contracts/manifests/social.contract.ts +4 -2
- package/src/contracts/manifests/steam.contract.ts +7 -3
- package/src/contracts/web-surface.ts +59 -0
- package/src/core/budget.ts +19 -9
- package/src/core/canonical-scripts.ts +1 -1
- package/src/core/check-runner.ts +63 -25
- package/src/core/config-rewrite.ts +37 -24
- package/src/core/discover-checks.ts +31 -7
- package/src/core/layout.ts +2 -0
- package/src/core/legacy-shapes.ts +39 -10
- package/src/core/motherhome.ts +88 -66
- package/src/core/one-way-in-policy.ts +2 -2
- package/src/core/outcome-words.ts +18 -2
- package/src/core/port-check-cli.ts +52 -0
- package/src/core/port-check-file.ts +295 -0
- package/src/core/port-check-files.ts +31 -0
- package/src/core/retention.ts +0 -14
- package/src/core/run-check.ts +14 -14
- package/src/core/run-ci.ts +11 -7
- package/src/core/run-fix.ts +30 -23
- package/src/core/run-fixers.ts +42 -5
- package/src/core/runs-matrix.ts +179 -0
- package/src/core/runs.ts +242 -0
- package/src/core/update-policy.ts +20 -0
- package/src/core/when-block.ts +29 -30
- package/src/core/when.ts +41 -103
- package/src/core/wire.ts +34 -9
- package/src/fleet/scan-fleet.ts +20 -4
- package/src/fleet/update-fleet.ts +6 -6
- package/src/home/activity-shape.ts +4 -1
- package/src/home/activity.ts +22 -15
- package/src/home/gui-app/ShipkitGui.swift +144 -25
- package/src/home/gui-app.ts +93 -30
- package/src/home/home-launchd.ts +30 -6
- package/src/home/home-routes.ts +14 -2
- package/src/home/home-start.ts +33 -5
- package/src/home/hubs.ts +5 -5
- package/src/home/machine-stores.ts +4 -2
- package/src/home/open-app.ts +23 -10
- package/src/home/pm-config.ts +15 -11
- package/src/home/pm-inputs.ts +76 -0
- package/src/home/pm.ts +11 -3
- package/src/home/process-rows.ts +23 -6
- package/src/home/resources.ts +67 -15
- package/src/home/run-daemon.ts +6 -1
- package/src/home/run-pm.ts +30 -5
- package/src/home/serve-web.ts +2 -2
- package/src/home/supervise.ts +24 -1
- package/src/home/widgets.ts +32 -3
- package/src/hooks/commit-message.ts +2 -3
- package/src/hooks/run-hooks.ts +60 -6
- package/src/init-repo.ts +6 -6
- package/src/ledger/gate-checks.ts +115 -61
- package/src/ledger/git-key.ts +5 -1
- package/src/ledger/ledger.ts +5 -5
- package/src/ledger/store.ts +11 -0
- package/src/ledger/toolchain-identity.ts +45 -27
- package/src/ledger/world-cache.ts +17 -15
- package/src/plan/repo-source.ts +18 -39
- package/src/release/run-release.ts +5 -0
- package/src/repo-config.ts +36 -56
- package/src/rules/rule.ts +2 -2
- package/src/scaffold/files/agent-coding.md +1 -1
- package/src/scaffold/files/agent-index.md +1 -1
- package/src/scaffold/files/verify-skill.md +2 -2
- package/src/serve/hub-app/activity-st.ts +5 -4
- package/src/serve/hub-app/activity-view.tsx +4 -4
- package/src/serve/hub-app/admin-view.tsx +13 -11
- package/src/serve/hub-app/check-view.tsx +66 -46
- package/src/serve/hub-app/check-words.ts +1 -11
- package/src/serve/hub-app/checks-svc.ts +16 -36
- package/src/serve/hub-app/col-tables-view.tsx +131 -0
- package/src/serve/hub-app/col-tables.ts +115 -0
- package/src/serve/hub-app/cols-svc.ts +33 -4
- package/src/serve/hub-app/dock-st.ts +3 -2
- package/src/serve/hub-app/dock-view.tsx +36 -0
- package/src/serve/hub-app/dock.css +7 -1
- package/src/serve/hub-app/float-cards.tsx +30 -0
- package/src/serve/hub-app/floating.tsx +41 -4
- package/src/serve/hub-app/git-st.ts +42 -0
- package/src/serve/hub-app/git-view.tsx +61 -0
- package/src/serve/hub-app/hash-route.ts +45 -1
- package/src/serve/hub-app/header-bar.tsx +62 -72
- package/src/serve/hub-app/health-svc.ts +3 -2
- package/src/serve/hub-app/home-st.ts +15 -3
- package/src/serve/hub-app/home-strip.css +69 -19
- package/src/serve/hub-app/home-strip.tsx +192 -118
- package/src/serve/hub-app/home.html +1 -0
- package/src/serve/hub-app/hub-app.tsx +21 -5
- package/src/serve/hub-app/hub-types.ts +11 -5
- package/src/serve/hub-app/hub.css +88 -44
- package/src/serve/hub-app/icons.tsx +15 -0
- package/src/serve/hub-app/index.html +1 -0
- package/src/serve/hub-app/keys.ts +11 -8
- package/src/serve/hub-app/live-svc.ts +3 -2
- package/src/serve/hub-app/machine-st.ts +34 -49
- package/src/serve/hub-app/palette-st.ts +5 -5
- package/src/serve/hub-app/palette-view.tsx +1 -1
- package/src/serve/hub-app/plan-view.tsx +51 -0
- package/src/serve/hub-app/planned-st.ts +9 -4
- package/src/serve/hub-app/processes-view.tsx +38 -0
- package/src/serve/hub-app/project-list.tsx +3 -3
- package/src/serve/hub-app/projects-svc.ts +15 -10
- package/src/serve/hub-app/prose-hits-st.ts +3 -2
- package/src/serve/hub-app/reach.ts +36 -0
- package/src/serve/hub-app/readme-svc.ts +3 -2
- package/src/serve/hub-app/repo-sections.ts +117 -0
- package/src/serve/hub-app/repo-st.ts +47 -10
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/sidebar.tsx +12 -37
- package/src/serve/hub-app/timeline-svc.ts +3 -2
- package/src/serve/hub-app/timeline-view.tsx +14 -11
- package/src/serve/hub-app/tree-build.ts +149 -155
- package/src/serve/hub-app/tree-row.tsx +39 -9
- package/src/serve/hub-app/tree-svc.ts +70 -49
- package/src/serve/hub-app/when-view.tsx +89 -0
- package/src/serve/hub-app/when.css +17 -0
- package/src/serve/hub-runtime.ts +3 -16
- package/src/serve/project-gutter.ts +1 -2
- package/src/serve/run-serve.ts +1 -1
- package/src/serve/serve-hub.ts +4 -14
- package/src/serve/warm-cell.ts +49 -16
- package/src/suite/judge-suites.ts +42 -17
- package/src/suite/suite-cli.ts +5 -8
- package/src/wiki/lint-md.ts +2 -0
- package/src/wiki/scan-md.ts +37 -10
- package/src/serve/hub-app/processes-svc.ts +0 -63
- package/src/serve/hub-app/repo-sections.tsx +0 -61
- package/src/serve/repo-processes.ts +0 -89
package/src/check.ts
CHANGED
|
@@ -14,14 +14,14 @@
|
|
|
14
14
|
// `cmd` argv, a subprocess, exit code is truth
|
|
15
15
|
//
|
|
16
16
|
// What a check declares about ITSELF is what every surface reads: `blocking`
|
|
17
|
-
// (can it stop you),
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
17
|
+
// (can it stop you), `runs` (every context it runs in), `cache` (how its
|
|
18
|
+
// answer is kept), `fix` (which command applies its repair), `applies` (does
|
|
19
|
+
// this repo give it work), `deps` (what must pass first). Every one of them is
|
|
20
|
+
// explicit, with no default (core/runs.ts). The file's own path joins its
|
|
21
|
+
// cache key automatically, so nothing declares its own code.
|
|
21
22
|
import type { CheckCtx } from './core/check-ctx.ts'
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import { FOREVER, parseDuration, type Line } from './core/wire.ts'
|
|
23
|
+
import { cacheProblem, combinationProblem, runsProblem, type Applies, type CacheDecl, type FixWhen, type Runs } from './core/runs.ts'
|
|
24
|
+
import { type Line } from './core/wire.ts'
|
|
25
25
|
import type { RepoConfig } from './repo-config.ts'
|
|
26
26
|
|
|
27
27
|
export { defineScan, mdPages, packageJson, type MdPage, type PackageJson, type Scan } from './scan.ts'
|
|
@@ -77,7 +77,7 @@ export type CheckDef = {
|
|
|
77
77
|
* gate asks "is my change good" and builds, `shipkit check` asks "is
|
|
78
78
|
* this repo healthy" and reports, and a tool chain has no business in
|
|
79
79
|
* the second question. A floor slot derives it, a file declares it, and
|
|
80
|
-
* it takes the 600s ceiling rather than the 120s one. `
|
|
80
|
+
* it takes the 600s ceiling rather than the 120s one. its `runs` say
|
|
81
81
|
* TIME (always, release, health, off). A build step on `health` is an
|
|
82
82
|
* opt in one: the gate parks it and `shipkit ci --with <id>` runs it. */
|
|
83
83
|
build?: boolean
|
|
@@ -85,74 +85,34 @@ export type CheckDef = {
|
|
|
85
85
|
* so a check naming none is a root of it, and a dep doubles as the race
|
|
86
86
|
* guard between two checks that touch one file. */
|
|
87
87
|
deps?: string[]
|
|
88
|
-
/** WHERE
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
|
|
106
|
-
/** a DECIDED never-cacheable, with the reason. Says somebody looked and
|
|
107
|
-
* no key can describe this, which takes it off the burn-down list. */
|
|
108
|
-
never?: string
|
|
88
|
+
/** WHERE IT RUNS, every context explicit, no default (core/runs.ts owns
|
|
89
|
+
* the meaning of each key and each value). A repo moves a check it does
|
|
90
|
+
* not own OFF with `off` in repo.config.ts, never elsewhere. */
|
|
91
|
+
runs: Runs
|
|
92
|
+
/** HOW ITS ANSWER IS KEPT, one variant, every parameter explicit:
|
|
93
|
+
* `files` (keyed on these paths), `reads` (keyed on what the body read),
|
|
94
|
+
* `ttl` (a world answer that keeps a span), `fact` (a decided value, once
|
|
95
|
+
* per key), `never` (with the reason). The file's own path joins every
|
|
96
|
+
* key automatically, so nothing declares its own code. */
|
|
97
|
+
cache: CacheDecl
|
|
98
|
+
/** which command applies `repair`: `'none'` (no repair), `'ci'` (the
|
|
99
|
+
* gate's fix pass and `shipkit check --fix`), `'manual'` (only
|
|
100
|
+
* `shipkit check --fix <id>`) */
|
|
101
|
+
fix: FixWhen
|
|
102
|
+
/** whether this repo gives the check anything to do: `'always'`, or a
|
|
103
|
+
* pure function of { root, config } returning the reason it does not
|
|
104
|
+
* (the row is then n/a in every context), or null */
|
|
105
|
+
applies: Applies
|
|
109
106
|
/** it REWRITES the repo while it runs (updates a dependency, untracks a
|
|
110
107
|
* file) unless the run is frozen, so a frozen run asks it a different
|
|
111
108
|
* question: only a check declaring this carries `frozen` in its cache
|
|
112
|
-
* key,
|
|
113
|
-
* a normal one. Declared, never guessed from the body. */
|
|
109
|
+
* key. Declared, never guessed from the body. */
|
|
114
110
|
writes?: boolean
|
|
115
|
-
/**
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
|
|
120
|
-
* change only. Or a PURE function of its last result (`status`, `at`, `streak`
|
|
121
|
-
* of that same status) and whether the repo is active, returning any of
|
|
122
|
-
* those: `(last) => (last?.status === 'fail' ? '1h' : '7d')`. A lambda
|
|
123
|
-
* that throws is an error row on the planned page, and the check runs on
|
|
124
|
-
* change only. Absent: its `ttl`, else a build step or a check keyed on
|
|
125
|
-
* its files waits for a save. A repo check nothing can key runs about
|
|
126
|
-
* once a day until it declares one, and a built-in never may: `shipkit
|
|
127
|
-
* keys --burn-down` names both. */
|
|
128
|
-
every?: Every
|
|
129
|
-
/** a time of day or week for the rare check that needs one, as five cron
|
|
130
|
-
* fields in local time (`'0 9 * * 1'`, Mondays at 9). Never with `every`. */
|
|
131
|
-
cron?: string
|
|
132
|
-
/** what a due check does while its repo is inactive (no agent session in
|
|
133
|
-
* it, VS Code does not have it open, it is not flagged active): `'wait'`
|
|
134
|
-
* turns it late and runs it once the repo is active again, `'run'` runs
|
|
135
|
-
* it on time anyway (a credential probe, an api key that must keep
|
|
136
|
-
* working). A pure function of the last result may choose. Default
|
|
137
|
-
* `'wait'`. */
|
|
138
|
-
whenInactive?: WhenInactiveDecl
|
|
139
|
-
/** why this repo gives the check nothing to do on a clock, or null: the
|
|
140
|
-
* plan leaves it out there (no row, no timer), and a gate or a health
|
|
141
|
-
* pass still runs it. Read at every reload of the repo's checks, so a
|
|
142
|
-
* file that gives it work (an ask with a probe, a release unit) plans it
|
|
143
|
-
* again. Cheap and pure over the repo's files and config */
|
|
144
|
-
planOff?: (env: CheckEnv) => string | null
|
|
145
|
-
/** cache-identity contribution for a `ttl` check whose verdict depends on
|
|
146
|
-
* config: a changed key busts the answer instantly instead of serving
|
|
147
|
-
* a stale cached assertion for the rest of the TTL. */
|
|
148
|
-
cacheKey?: (env: CheckEnv) => string
|
|
149
|
-
/** idempotent auto-repair, never interactive. `shipkit check --fix <id>`
|
|
150
|
-
* applies it, then re-runs the check. It gets the same `c` as the body,
|
|
151
|
-
* writes with `c.write` and `c.remove`, and is never cached. An `fs`
|
|
152
|
-
* import in the check file would keep an undeclared body off its reads
|
|
153
|
-
* (core/raw-access.ts). A repair that needs
|
|
154
|
-
* judgement stays a FAIL with a pointer. */
|
|
155
|
-
fix?: (c: CheckCtx) => FixResult | Promise<FixResult>
|
|
111
|
+
/** idempotent auto-repair, never interactive, required when `fix` is not
|
|
112
|
+
* `'none'`. It gets the same `c` as the body, writes with `c.write` and
|
|
113
|
+
* `c.remove`, and is never cached. A repair that needs judgement stays a
|
|
114
|
+
* FAIL with a pointer. */
|
|
115
|
+
repair?: (c: CheckCtx) => FixResult | Promise<FixResult>
|
|
156
116
|
/** wall clock ceiling in ms. A `cmd` check is killed with its whole
|
|
157
117
|
* process tree at the ceiling. */
|
|
158
118
|
timeoutMs?: number
|
|
@@ -160,7 +120,8 @@ export type CheckDef = {
|
|
|
160
120
|
* passes over it prints a WARN and reads yellow, and never blocks.
|
|
161
121
|
* Judged on the wall time of a run that measured, at the quick tier.
|
|
162
122
|
* Default 2000 for an in process body, 10000 for a build step, a `cmd`
|
|
163
|
-
* or a world probe (
|
|
123
|
+
* or a world probe (cache `ttl`, `fact` or `never`, core/budget.ts).
|
|
124
|
+
* The repo's `budgets` wins over it. */
|
|
164
125
|
budgetMs?: number
|
|
165
126
|
/** a label several checks share, so `--skip <group>` parks all of them
|
|
166
127
|
* and `--only <group>` runs all of them. */
|
|
@@ -197,6 +158,19 @@ export function describeFirst(...candidates: (string | null | undefined)[]): str
|
|
|
197
158
|
throw new Error(`no description candidate passes the shape: ${candidates.map((d) => JSON.stringify(d)).join(', ')}`)
|
|
198
159
|
}
|
|
199
160
|
|
|
161
|
+
/** the keys the explicit declaration replaced, and what replaces each */
|
|
162
|
+
const RETIRED_KEYS: Record<string, string> = {
|
|
163
|
+
when: 'runs (every context explicit)',
|
|
164
|
+
every: 'runs.onClock { every, inactive }',
|
|
165
|
+
cron: 'runs.onClock { every: <cron>, inactive }',
|
|
166
|
+
whenInactive: 'runs.onClock.inactive',
|
|
167
|
+
planOff: 'applies',
|
|
168
|
+
inputs: "cache { by: 'files', inputs, key }",
|
|
169
|
+
ttl: "cache { by: 'ttl', span, key } or { by: 'fact', key }",
|
|
170
|
+
never: "cache { by: 'never', why }",
|
|
171
|
+
cacheKey: 'cache.key',
|
|
172
|
+
}
|
|
173
|
+
|
|
200
174
|
/** THE way a check file declares itself. It validates the shape at import
|
|
201
175
|
* time, so a malformed check dies when discovery loads it and never mid
|
|
202
176
|
* gate. The id, the title and the owner are filled in by discovery, which
|
|
@@ -207,20 +181,21 @@ export function check(d: CheckDef): CheckDef {
|
|
|
207
181
|
const bad = descriptionProblem(d.description)
|
|
208
182
|
if (bad != null) throw new Error(`check${name}: ${bad}`)
|
|
209
183
|
if (typeof d.blocking !== 'boolean')
|
|
210
|
-
throw new Error(`check${
|
|
184
|
+
throw new Error(`check${name}: blocking is required — true (about this repo, it stops you) or false (about the world, it warns)`)
|
|
211
185
|
if (has('run') === has('cmd'))
|
|
212
|
-
throw new Error(`check${
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
186
|
+
throw new Error(`check${name}: declare exactly one body, \`run\` (in process) or \`cmd\` (argv)`)
|
|
187
|
+
const raw = d as unknown as Record<string, unknown>
|
|
188
|
+
const retired = Object.keys(RETIRED_KEYS).filter((k) => raw[k] !== undefined)
|
|
189
|
+
if (retired.length > 0) throw new Error(`check${name}: ${retired.map((k) => `\`${k}\` is retired, declare ${RETIRED_KEYS[k]}`).join('; ')} (build/shipkit/runs.md)`)
|
|
190
|
+
if (typeof raw.fix === 'function') throw new Error(`check${name}: \`fix\` is now the word 'none' | 'ci' | 'manual', and the function is \`repair\` (build/shipkit/runs.md)`)
|
|
191
|
+
const runs = runsProblem(d.runs)
|
|
192
|
+
if (runs != null) throw new Error(`check${name}: ${runs}`)
|
|
193
|
+
const cache = cacheProblem(d.cache)
|
|
194
|
+
if (cache != null) throw new Error(`check${name}: ${cache}`)
|
|
195
|
+
if (d.applies !== 'always' && typeof d.applies !== 'function')
|
|
196
|
+
throw new Error(`check${name}: applies is required: 'always', or a function of { root, config } returning why this repo gives it nothing to do, or null`)
|
|
197
|
+
const whole = combinationProblem({ runs: d.runs, cache: d.cache, fix: d.fix, repair: d.repair, build: d.build === true, body: has('run') ? 'run' : 'cmd', blocking: d.blocking })
|
|
198
|
+
if (whole != null) throw new Error(`check${name}: ${whole}`)
|
|
224
199
|
return d
|
|
225
200
|
}
|
|
226
201
|
|
|
@@ -232,7 +207,7 @@ export function check(d: CheckDef): CheckDef {
|
|
|
232
207
|
|
|
233
208
|
export type TaskDef = {
|
|
234
209
|
/** the name every surface uses: the gate line, `--only`, `--skip`, a
|
|
235
|
-
* `deps` entry,
|
|
210
|
+
* `deps` entry, an `off` key and the ledger row. Unique in the repo. */
|
|
236
211
|
id: string
|
|
237
212
|
/** one line stating what must be true (the shape: descriptionProblem).
|
|
238
213
|
* A floor slot and a unit test check get theirs from the
|
|
@@ -254,7 +229,7 @@ export type TaskDef = {
|
|
|
254
229
|
/** a label several tasks share, so `--skip <group>` parks all of them at
|
|
255
230
|
* once (a local bench, a slow integration tier). */
|
|
256
231
|
group?: string
|
|
257
|
-
/** DEPRECATED, read
|
|
232
|
+
/** DEPRECATED, read as every gate axis false: out of the gate until
|
|
258
233
|
* `--with <id>` names it. */
|
|
259
234
|
optIn?: boolean
|
|
260
235
|
/** wall clock ceiling in ms, default 600000 for a build step. One that
|
|
@@ -332,13 +307,12 @@ export type Rule = {
|
|
|
332
307
|
|
|
333
308
|
// ── running a body, and what every line it prints carries ────────────────
|
|
334
309
|
|
|
335
|
-
/** the config part of a check's cache identity
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
return at.at === 'off' ? `${key ?? ''}|exempt:${at.reason ?? ''}` : `${key ?? ''}|when:${at.at}`
|
|
310
|
+
/** the config part of a check's cache identity: its declared key, and the
|
|
311
|
+
* reason it is off. Turning a check off, or editing why, asks the question
|
|
312
|
+
* again rather than serving the answer from before. Where it runs is not
|
|
313
|
+
* part of it: the answer to one question does not depend on who asks. */
|
|
314
|
+
export function checkCacheExtra(key: string | undefined, off: string | null): string | undefined {
|
|
315
|
+
return off == null ? key : `${key ?? ''}|exempt:${off}`
|
|
342
316
|
}
|
|
343
317
|
|
|
344
318
|
/** THE one way to run an in-process body (the check suite and the `--fix`
|
|
@@ -373,12 +347,13 @@ export async function runCheckBody(
|
|
|
373
347
|
}
|
|
374
348
|
|
|
375
349
|
/** tag every severity line with the check id; a fix pointer of a fixable check
|
|
376
|
-
* advertises the autofix
|
|
377
|
-
*
|
|
350
|
+
* advertises the autofix, unless it names its own `--fix` or says the repair
|
|
351
|
+
* is `by hand`. Idempotent, since an already-tagged line passes back through
|
|
352
|
+
* here on a cached suite replay. */
|
|
378
353
|
export function tagCheckLines(u: { id: string; fix?: unknown }, lines: Line[]): Line[] {
|
|
379
354
|
return lines.map((l) => {
|
|
380
355
|
if (l.kind === 'fix-warn' || l.kind === 'fix-fail')
|
|
381
|
-
return u.fix == null || l.text.includes('--fix ')
|
|
356
|
+
return u.fix == null || l.text.includes('--fix ') || /\bby hand\b/.test(l.text)
|
|
382
357
|
? l
|
|
383
358
|
: { ...l, text: `${l.text} · autofix: shipkit check --fix ${u.id}` }
|
|
384
359
|
if (l.kind === 'note') return l
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// what it read: package.json, the probes below, the test file listing, and
|
|
6
6
|
// whatever the floor resolution read.
|
|
7
7
|
//
|
|
8
|
-
// It also enforces the package.json half of ONE WAY IN (build/shipkit.md):
|
|
8
|
+
// It also enforces the package.json half of ONE WAY IN (build/shipkit/one-way-in.md):
|
|
9
9
|
// `test`, `lint` and `typecheck` ROUTE to `shipkit ci --only <id>` and the
|
|
10
10
|
// tool body they used to hold sits at `<id>:raw`. That is what makes a cache
|
|
11
11
|
// worth having: `bun run test` is the reflex, and the reflex has to land in
|
|
@@ -107,8 +107,11 @@ export function evaluatePackageJson(t: CheckCtx): void {
|
|
|
107
107
|
|
|
108
108
|
// `test` is asked for only once the repo HAS tests: the fixer deliberately
|
|
109
109
|
// refuses to scaffold it before that (a fixer never manufactures a
|
|
110
|
-
// failure), so nagging for it would be a WARN no machine may clear
|
|
111
|
-
|
|
110
|
+
// failure), so nagging for it would be a WARN no machine may clear. For
|
|
111
|
+
// the same reason `lint` and `fmt` wait for a source file to read
|
|
112
|
+
const tests = hasTestFiles(testFiles(t))
|
|
113
|
+
const source = tests || hasSourceFiles(t)
|
|
114
|
+
const baseline = BASELINE.filter((n) => (n === 'test' ? tests : source))
|
|
112
115
|
const missingBaseline = baseline.filter((n) => !has(n))
|
|
113
116
|
if (missingBaseline.length > 0)
|
|
114
117
|
t.warn(`package.json: missing baseline script(s): ${missingBaseline.join(', ')}`)
|
|
@@ -173,18 +176,37 @@ export function hasTestFiles(paths: readonly string[]): boolean {
|
|
|
173
176
|
})
|
|
174
177
|
}
|
|
175
178
|
|
|
179
|
+
const SOURCE_GLOB = '**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'
|
|
180
|
+
/** an empty module: nothing but `export {}`, semicolons and whitespace */
|
|
181
|
+
const EMPTY_MODULE = /^(?:\s|;|export\s*\{\s*\})*$/
|
|
182
|
+
|
|
183
|
+
/** a code file with something in it, outside installs, build output, dot dirs
|
|
184
|
+
* and the repo config: what lint and fmt would read. Stops at the first */
|
|
185
|
+
function hasSourceFiles(c: CheckCtx): boolean {
|
|
186
|
+
for (const f of c.list(SOURCE_GLOB)) {
|
|
187
|
+
const parts = f.split('/')
|
|
188
|
+
if (f === 'repo.config.ts' || !parts.slice(0, -1).every((d) => !TEST_SKIP.has(d) && !d.startsWith('.'))) continue
|
|
189
|
+
if (!EMPTY_MODULE.test(c.read(f) ?? '')) return true
|
|
190
|
+
}
|
|
191
|
+
return false
|
|
192
|
+
}
|
|
193
|
+
|
|
176
194
|
export const packageJsonShape = check({
|
|
177
195
|
id: 'SK001',
|
|
178
196
|
blocking: true,
|
|
179
197
|
title: 'package.json',
|
|
180
198
|
description: 'package.json routes lint, typecheck and test through the gate',
|
|
181
199
|
owner: OWNER,
|
|
200
|
+
runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
|
|
201
|
+
cache: { by: 'reads' },
|
|
202
|
+
fix: 'ci',
|
|
203
|
+
applies: 'always',
|
|
182
204
|
run: (c) => evaluatePackageJson(c),
|
|
183
205
|
// mechanical repairs only: field flips + scripts whose canonical body the
|
|
184
206
|
// vocabulary page states. Bodies it does NOT state (assets, comfy:up) are
|
|
185
207
|
// named in the summary, never invented. rvlib-* roots untouched — their
|
|
186
208
|
// license/private are publish decisions.
|
|
187
|
-
|
|
209
|
+
repair: (c): FixResult => {
|
|
188
210
|
const raw = c.read('package.json')
|
|
189
211
|
if (raw == null) return { changed: false, summary: 'no package.json — nothing to fix' }
|
|
190
212
|
if (rootPublishesNpm(c.config)) return { changed: false, summary: 'published to npm: its package.json is its public contract, never rewritten' }
|
|
@@ -16,6 +16,10 @@ export const contractsFulfillness = check({
|
|
|
16
16
|
owner: 'meta/contracts.md',
|
|
17
17
|
// keyed on what the contracts read: each artifact, each detector, and each
|
|
18
18
|
// folder a glob walked through `c.entries`, a gitignored one included
|
|
19
|
+
runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
|
|
20
|
+
cache: { by: 'reads' },
|
|
21
|
+
fix: 'none',
|
|
22
|
+
applies: 'always',
|
|
19
23
|
run: async (c) => {
|
|
20
24
|
applyContractsResult(c, await c.use(contractsResult), contractsFulfillness)
|
|
21
25
|
},
|
|
@@ -20,6 +20,10 @@ export const claudeMdShape = check({
|
|
|
20
20
|
owner: 'meta/agent-folder.md',
|
|
21
21
|
// keyed on what it reads: CLAUDE.md and whether agent/index.md exists. The
|
|
22
22
|
// gitignore probe runs git, so an answer that took it is never stored
|
|
23
|
+
runs: { onPreCommit: { budgetMs: 40 }, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
|
|
24
|
+
cache: { by: 'reads' },
|
|
25
|
+
fix: 'ci',
|
|
26
|
+
applies: 'always',
|
|
23
27
|
run: async (c) => {
|
|
24
28
|
const hasAgent = c.exists('agent/index.md')
|
|
25
29
|
const claude = c.read('CLAUDE.md')
|
|
@@ -53,7 +57,7 @@ export const claudeMdShape = check({
|
|
|
53
57
|
// CLAUDE.md would lint green with the base include forever missing,
|
|
54
58
|
// trading a loud FAIL for a permanent silent gap. Creation and prose
|
|
55
59
|
// relocation stay human/machine-local (`shipkit start --include`).
|
|
56
|
-
|
|
60
|
+
repair: (c): FixResult => {
|
|
57
61
|
if (!c.exists('agent/index.md')) return { changed: false, summary: 'no agent/index.md — nothing mechanical to fix' }
|
|
58
62
|
const cur = c.read('CLAUDE.md')
|
|
59
63
|
if (cur == null) return { changed: false, summary: 'CLAUDE.md missing — creation needs the base include (shipkit start <name> writes it), not autofixable' }
|
|
@@ -205,11 +205,14 @@ export const gitignorePolicy = check({
|
|
|
205
205
|
title: '.gitignore',
|
|
206
206
|
description: 'the managed .gitignore block keeps private contract folders out of git',
|
|
207
207
|
owner: 'meta/contracts.md',
|
|
208
|
-
// .gitignore plus what git currently TRACKS and IGNORES, which the tree
|
|
209
|
-
// carries and no read set records (the header says why)
|
|
210
|
-
inputs: ['.'],
|
|
211
208
|
// it untracks generated files (a commit) unless the run is frozen
|
|
212
209
|
writes: true,
|
|
210
|
+
runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
|
|
211
|
+
// .gitignore plus what git currently TRACKS and IGNORES, which the tree
|
|
212
|
+
// carries and no read set records (the header says why)
|
|
213
|
+
cache: { by: 'files', inputs: ['.'], key: null },
|
|
214
|
+
fix: 'ci',
|
|
215
|
+
applies: 'always',
|
|
213
216
|
run: async (c) => {
|
|
214
217
|
// a generated path still in the index is the half-applied state the
|
|
215
218
|
// managed block cannot fix on its own
|
|
@@ -278,7 +281,7 @@ export const gitignorePolicy = check({
|
|
|
278
281
|
}
|
|
279
282
|
c.ok(`.gitignore policy block current, ${claimed.length + backProbes.size} patterns verified against git`)
|
|
280
283
|
},
|
|
281
|
-
|
|
284
|
+
repair: async (c): Promise<FixResult> => {
|
|
282
285
|
// the move first: it writes the new block itself, before any folder moves
|
|
283
286
|
const moved = process.env.SHIPKIT_FROZEN === '1' ? null : migrateLayout(c.root, { config: c.config, renderGitignore: (t, legacy, cfg) => renderPolicy(t, cfg ?? c.config, legacy) })
|
|
284
287
|
const layout = moved == null ? null
|
|
@@ -10,7 +10,12 @@
|
|
|
10
10
|
// Per rvlib-* dep:
|
|
11
11
|
// link: → the symlink must resolve (a broken link dies cryptically at
|
|
12
12
|
// import time otherwise) — nothing to update, code is live
|
|
13
|
-
// semver → installed vs npm latest
|
|
13
|
+
// semver → installed vs npm latest. A semver pin whose node_modules
|
|
14
|
+
// entry is a `bun link` symlink is judged by the PIN's floor
|
|
15
|
+
// instead: the link carries the source, always the newest, while
|
|
16
|
+
// a fresh clone installs the pin. Behind, the pin moves through
|
|
17
|
+
// the same guarded update, and `bun link <name>` restores the
|
|
18
|
+
// link after it (a link that does not come back FAILs). A BREAKING jump (a new major, or a
|
|
14
19
|
// new 0.x minor) is never taken by itself
|
|
15
20
|
// (src/core/update-policy.ts: WARN naming `fleet --update --major`,
|
|
16
21
|
// package.json untouched); the rvlib-shipkit tree running this
|
|
@@ -46,7 +51,7 @@
|
|
|
46
51
|
import { existsSync, readFileSync } from 'fs'
|
|
47
52
|
import { basename, join } from 'path'
|
|
48
53
|
import type { CheckCtx } from '../core/check-ctx.ts'
|
|
49
|
-
import { allowMajor, breakingAdvice, isBreaking } from '../core/update-policy.ts'
|
|
54
|
+
import { allowMajor, breakingAdvice, isBreaking, isLinkedInstall, pinFloor } from '../core/update-policy.ts'
|
|
50
55
|
import { isRunningCopy, rememberLatest } from '../core/self-update.ts'
|
|
51
56
|
import { rootPublishesNpm } from '../core/repo-facts.ts'
|
|
52
57
|
import { mutateLease, withMutateLease } from '../core/locked-write.ts'
|
|
@@ -61,6 +66,13 @@ const installedVersion = (root: string, name: string): string | null => {
|
|
|
61
66
|
try { return (JSON.parse(readFileSync(p, 'utf8')) as Pkg).version ?? null } catch { return null }
|
|
62
67
|
}
|
|
63
68
|
|
|
69
|
+
const readPin = (root: string, name: string): string | null => {
|
|
70
|
+
try {
|
|
71
|
+
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as Pkg
|
|
72
|
+
return pkg.dependencies?.[name] ?? pkg.devDependencies?.[name] ?? null
|
|
73
|
+
} catch { return null }
|
|
74
|
+
}
|
|
75
|
+
|
|
64
76
|
async function latestVersion(t: CheckCtx, name: string): Promise<string> {
|
|
65
77
|
const res = await t.fetch(`https://registry.npmjs.org/${name}/latest`)
|
|
66
78
|
if (!res.ok) throw new Error(`registry ${res.status}`)
|
|
@@ -74,23 +86,24 @@ export const rvlibVersions = check({
|
|
|
74
86
|
blocking: true,
|
|
75
87
|
title: 'rvlib versions',
|
|
76
88
|
description: 'the rvlib packages installed here are as new as the published ones',
|
|
77
|
-
owner: 'build/shipkit.md',
|
|
78
|
-
ttl: '24h',
|
|
89
|
+
owner: 'build/shipkit/consuming.md',
|
|
79
90
|
// it updates the rvlib deps unless the run is frozen
|
|
80
91
|
writes: true,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
try { pkg = JSON.parse(readFileSync(join(env.root, 'package.json'), 'utf8')) as Pkg } catch { return 'no readable package.json' }
|
|
84
|
-
return Object.keys({ ...pkg.dependencies, ...pkg.devDependencies }).some((n) => n.startsWith('rvlib-')) ? null : 'no rvlib-* dependency'
|
|
85
|
-
},
|
|
86
|
-
cacheKey: (env) => {
|
|
92
|
+
runs: { onPreCommit: false, onCi: false, onFull: true, onRelease: true, onHealth: true, onSave: false, onClock: { every: '24h', inactive: 'wait' } },
|
|
93
|
+
cache: { by: 'ttl', span: '24h', key: (env) => {
|
|
87
94
|
let pkg: Pkg
|
|
88
95
|
try { pkg = JSON.parse(readFileSync(join(env.root, 'package.json'), 'utf8')) as Pkg } catch { return 'unreadable' }
|
|
89
96
|
return Object.keys({ ...pkg.dependencies, ...pkg.devDependencies })
|
|
90
97
|
.filter((n) => n.startsWith('rvlib-'))
|
|
91
98
|
.sort()
|
|
92
|
-
.map((n) => `${n}@${installedVersion(env.root, n) ?? 'absent'}`)
|
|
99
|
+
.map((n) => `${n}@${installedVersion(env.root, n) ?? 'absent'}${isLinkedInstall(env.root, n) ? ` pin ${readPin(env.root, n)}` : ''}`)
|
|
93
100
|
.join(',') + (rootPublishesNpm(env.config) ? ' npm' : '')
|
|
101
|
+
} },
|
|
102
|
+
fix: 'none',
|
|
103
|
+
applies: (env) => {
|
|
104
|
+
let pkg: Pkg
|
|
105
|
+
try { pkg = JSON.parse(readFileSync(join(env.root, 'package.json'), 'utf8')) as Pkg } catch { return 'no readable package.json' }
|
|
106
|
+
return Object.keys({ ...pkg.dependencies, ...pkg.devDependencies }).some((n) => n.startsWith('rvlib-')) ? null : 'no rvlib-* dependency'
|
|
94
107
|
},
|
|
95
108
|
run: async (c) => {
|
|
96
109
|
let pkg: Pkg
|
|
@@ -100,39 +113,47 @@ export const rvlibVersions = check({
|
|
|
100
113
|
const pm = detectPackageManager(c.root)
|
|
101
114
|
const manifest = ['package.json', ...(isRefusal(pm) ? LOCK_FILES : pm.locks)]
|
|
102
115
|
|
|
103
|
-
const stale: Array<{ name: string; installed: string; latest: string }> = []
|
|
116
|
+
const stale: Array<{ name: string; installed: string; latest: string; linked: boolean }> = []
|
|
104
117
|
for (const [name, spec] of deps) {
|
|
105
118
|
if (spec.startsWith('link:')) {
|
|
106
119
|
if (installedVersion(c.root, name) == null) c.fail(`${name} is link: but the symlink does not resolve — re-run \`bun link ${name}\` (a broken link dies cryptically at import time)`)
|
|
107
120
|
else c.ok(`${name} (linked, live)`)
|
|
108
121
|
continue
|
|
109
122
|
}
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
123
|
+
const onDisk = installedVersion(c.root, name)
|
|
124
|
+
if (onDisk == null) { c.fail(`${name} declared (${spec}) but not installed — ${isRefusal(pm) ? pm.refused : `run: ${pm.install.join(' ')}`}`); continue }
|
|
125
|
+
const linked = isLinkedInstall(c.root, name)
|
|
126
|
+
const floor = linked ? pinFloor(spec) : null
|
|
127
|
+
if (linked && floor == null) { c.ok(`${name} (linked to its source, pin ${spec} names no version to judge)`); continue }
|
|
128
|
+
const installed = floor ?? onDisk
|
|
129
|
+
// how the lines below name it: a linked dep is its pin, never the source it links
|
|
130
|
+
const label = linked ? `${name} pin ${installed}` : `${name} ${installed}`
|
|
112
131
|
let latest: string
|
|
113
132
|
try { latest = await latestVersion(c, name) } catch (e) {
|
|
114
|
-
c.lines.push({ kind: 'skip', text: `${
|
|
133
|
+
c.lines.push({ kind: 'skip', text: `${label} — registry unreachable (${String(e).slice(0, 80)}), version drift unknown` })
|
|
115
134
|
continue
|
|
116
135
|
}
|
|
117
136
|
// real semver order, not string difference — a dist-tag pointing
|
|
118
137
|
// BEHIND the installed version (prerelease, unpublish window)
|
|
119
138
|
// must never trigger a silent downgrade
|
|
120
139
|
const cmp = Bun.semver.order(latest, installed)
|
|
121
|
-
if (cmp === 0) c.ok(`${
|
|
122
|
-
else if (cmp < 0) c.ok(`${
|
|
140
|
+
if (cmp === 0) c.ok(`${label} (latest${linked ? ', linked to its source' : ''})`)
|
|
141
|
+
else if (cmp < 0) c.ok(`${label} ahead of npm latest ${latest} (prerelease or dist-tag lag) — not touching`)
|
|
123
142
|
else if (isBreaking(installed, latest) && !allowMajor()) c.warn(breakingAdvice(name, latest, basename(c.root)))
|
|
124
|
-
|
|
143
|
+
// a linked toolkit moves only its pin: the source tree this process
|
|
144
|
+
// runs from is never written, so the swap below cannot happen
|
|
145
|
+
else if (name === 'rvlib-shipkit' && !linked && isRunningCopy(c.root)) {
|
|
125
146
|
// swapping this tree under the running process is how a run
|
|
126
147
|
// meets modules of two versions. The startup self-update owns
|
|
127
148
|
// it: handing it the answer makes the next run update and
|
|
128
149
|
// restart before any check loads
|
|
129
150
|
rememberLatest(latest)
|
|
130
151
|
c.warn(`${name} ${installed} → ${latest} is the toolkit running this check, so it is not swapped mid run: the next shipkit run updates it and restarts before any check`)
|
|
131
|
-
} else stale.push({ name, installed, latest })
|
|
152
|
+
} else stale.push({ name, installed, latest, linked })
|
|
132
153
|
}
|
|
133
154
|
if (stale.length === 0) return
|
|
134
155
|
|
|
135
|
-
const drift = stale.map((s) => `${s.name} ${s.installed} → ${s.latest}`).join(', ')
|
|
156
|
+
const drift = stale.map((s) => `${s.name}${s.linked ? ' pin' : ''} ${s.installed} → ${s.latest}`).join(', ')
|
|
136
157
|
const names = stale.map((s) => s.name)
|
|
137
158
|
if (isRefusal(pm)) {
|
|
138
159
|
c.warn(`newer rvlib live: ${drift}, not updating: ${pm.refused}`)
|
|
@@ -164,7 +185,11 @@ export const rvlibVersions = check({
|
|
|
164
185
|
const leased = await withMutateLease(mutateLease(c.root, 'fix'), { label: 'self-update', print: () => {}, escape: 'SHIPKIT_FROZEN=1' }, async () => {
|
|
165
186
|
const again = await c.exec(['git', 'status', '--porcelain', '--', ...manifest], { cwd: c.root })
|
|
166
187
|
if (again.code === 0 && again.out.trim() !== '') return null
|
|
167
|
-
|
|
188
|
+
const up = await c.exec(cmd, { cwd: c.root, timeoutMs: 120_000 })
|
|
189
|
+
// the update may swap a link for the tarball: put every link back,
|
|
190
|
+
// whatever the update did, inside the same lease
|
|
191
|
+
for (const s of stale) if (s.linked) await c.exec(['bun', 'link', s.name], { cwd: c.root, timeoutMs: 60_000 })
|
|
192
|
+
return up
|
|
168
193
|
})
|
|
169
194
|
if (!leased.ok) {
|
|
170
195
|
c.warn(`newer rvlib live: ${drift} — self-update deferred (${leased.warn}); re-run check once that run finishes`)
|
|
@@ -188,6 +213,13 @@ export const rvlibVersions = check({
|
|
|
188
213
|
return
|
|
189
214
|
}
|
|
190
215
|
for (const s of stale) {
|
|
216
|
+
if (s.linked) {
|
|
217
|
+
const now = pinFloor(readPin(c.root, s.name) ?? '')
|
|
218
|
+
if (!isLinkedInstall(c.root, s.name)) c.fail(`${s.name} pin ${s.installed} → ${now ?? 'unreadable'} but node_modules/${s.name} is no longer linked to its source — run: bun link ${s.name}`)
|
|
219
|
+
else if (now === s.latest) c.warn(`${s.name} pin ${s.installed} → ${now} self-updated, link to the source restored — commit the bump`)
|
|
220
|
+
else c.warn(`${s.name} updated but its pin reads ${now ?? 'unreadable'} (wanted ${s.latest}) — check package.json`)
|
|
221
|
+
continue
|
|
222
|
+
}
|
|
191
223
|
const now = installedVersion(c.root, s.name)
|
|
192
224
|
// WARN on purpose: the run MUTATED the repo and a follow-up
|
|
193
225
|
// (restart watchers, commit) is owed — an ok here would be a
|
|
@@ -40,6 +40,10 @@ export const npmPublishSafety = check({
|
|
|
40
40
|
owner: 'meta/rv-conventions/package-json.md',
|
|
41
41
|
// keyed on what it reads: the root package.json and every declared release
|
|
42
42
|
// unit's, at the dirs the config picks
|
|
43
|
+
runs: { onPreCommit: false, onCi: false, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
|
|
44
|
+
cache: { by: 'reads' },
|
|
45
|
+
fix: 'none',
|
|
46
|
+
applies: 'always',
|
|
43
47
|
run: (c) => {
|
|
44
48
|
const json = (rel: string): Pkg | null | 'unparseable' => {
|
|
45
49
|
try { return c.json<Pkg>(rel) } catch { return 'unparseable' }
|
|
@@ -27,8 +27,10 @@ export const githubVisibility = check({
|
|
|
27
27
|
title: 'github visibility',
|
|
28
28
|
description: 'the github visibility of this repo matches the one it declares',
|
|
29
29
|
owner: 'meta/contracts.md',
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
runs: { onPreCommit: false, onCi: false, onFull: false, onRelease: false, onHealth: true, onSave: 'moved', onClock: 'once' },
|
|
31
|
+
cache: { by: 'fact', key: (env) => declaredVisibility(env.config) },
|
|
32
|
+
fix: 'none',
|
|
33
|
+
applies: 'always',
|
|
32
34
|
run: async (c) => {
|
|
33
35
|
const declared = declaredVisibility(c.config)
|
|
34
36
|
const skip = (why: string): void => void c.lines.push({ kind: 'skip', text: `github visibility unverified — ${why} (declared: ${declared})` })
|
|
@@ -86,15 +86,17 @@ export const reviewerAgent = check({
|
|
|
86
86
|
title: 'reviewer definition',
|
|
87
87
|
description: 'the declared reviewer agent has its brief, its definition and read only tools',
|
|
88
88
|
owner: 'identity/how-we-work.md',
|
|
89
|
+
runs: { onPreCommit: { budgetMs: 20 }, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
|
|
89
90
|
// the answer is the config, the definition file and the brief. The brief
|
|
90
91
|
// is a CONFIG path that may sit outside the repo, so both files enter the
|
|
91
92
|
// key by content, wherever they live
|
|
92
|
-
inputs: [CONFIG_FILE],
|
|
93
|
-
cacheKey: (env) => {
|
|
93
|
+
cache: { by: 'files', inputs: [CONFIG_FILE], key: (env) => {
|
|
94
94
|
const r = env.config.reviewer
|
|
95
95
|
if (r == null) return 'no reviewer'
|
|
96
96
|
return `${r.name}|${(r.tools ?? REVIEWER_TOOLS).join(',')}|def=${fileStamp(join(env.root, reviewerAgentPath(r.name)))}|${r.brief}=${fileStamp(configPath(env.root, r.brief))}`
|
|
97
|
-
},
|
|
97
|
+
} },
|
|
98
|
+
fix: 'ci',
|
|
99
|
+
applies: 'always',
|
|
98
100
|
run: (c) => {
|
|
99
101
|
const r = c.config.reviewer
|
|
100
102
|
if (r == null) return c.ok('no reviewer agent declared (repo.config reviewer:) — skipped')
|
|
@@ -109,7 +111,7 @@ export const reviewerAgent = check({
|
|
|
109
111
|
// create-if-missing only: an existing definition may carry a repo's own
|
|
110
112
|
// tuned prompt, and silently rewriting that would cost more than the
|
|
111
113
|
// finding it repairs. Violations in a present file stay a loud FAIL.
|
|
112
|
-
|
|
114
|
+
repair: (c): FixResult => {
|
|
113
115
|
const r = c.config.reviewer
|
|
114
116
|
if (r == null) return { changed: false, summary: 'no reviewer declared — nothing to write' }
|
|
115
117
|
const rel = reviewerAgentPath(r.name)
|