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/core/retention.ts
CHANGED
|
@@ -16,8 +16,6 @@ import { JOBS, JOBS_KEEP_MS, JOBS_MAX, sweepJobs } from './jobs.ts'
|
|
|
16
16
|
import { CI_LOG_DIR } from './layout.ts'
|
|
17
17
|
import { compactDb, dbPath, shipkitRoot } from '../ledger/db.ts'
|
|
18
18
|
import { SCAN_INDEX_KEEP_MS, sweepScanIndexes } from '../ledger/git-scan.ts'
|
|
19
|
-
import { capLog, LOG_MAX_BYTES } from '../home/supervise.ts'
|
|
20
|
-
import { processDir } from '../serve/repo-processes.ts'
|
|
21
19
|
import { KEEP_MS as RECORD_KEEP_MS, MAX_ROWS, recordUse, sweepRuns } from '../ledger/run-log.ts'
|
|
22
20
|
import { answersDb, answersUse, importLegacyStore, KEEP_MS as ANSWERS_KEEP_MS, legacyStoreDir, MAX_ANSWERS, sweepAnswers } from '../ledger/store.ts'
|
|
23
21
|
|
|
@@ -169,18 +167,6 @@ export function repoStores(root: string): Store[] {
|
|
|
169
167
|
},
|
|
170
168
|
sweep: (now) => sweepJobs(root, now),
|
|
171
169
|
},
|
|
172
|
-
{
|
|
173
|
-
// the dev servers the hub runs for the repo (serve.processes)
|
|
174
|
-
id: 'process logs',
|
|
175
|
-
where: `${processDir(root)}/*.log`,
|
|
176
|
-
cap: { maxFileBytes: LOG_MAX_BYTES },
|
|
177
|
-
measure: () => walkUse(processDir(root), { match: (n) => n.endsWith('.log') }),
|
|
178
|
-
sweep: () => {
|
|
179
|
-
let names: string[] = []
|
|
180
|
-
try { names = readdirSync(processDir(root)) } catch { return }
|
|
181
|
-
for (const n of names) if (n.endsWith('.log')) capLog(join(processDir(root), n))
|
|
182
|
-
},
|
|
183
|
-
},
|
|
184
170
|
]
|
|
185
171
|
if (gitDir != null) {
|
|
186
172
|
const dir = join(gitDir, 'shipkit')
|
package/src/core/run-check.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { gateBinary } from '../ledger/toolchain-identity.ts'
|
|
|
17
17
|
import { runChecks, selectChecks, type Audience, type CheckEvent, type CheckResult, type RunnableCheck } from './check-runner.ts'
|
|
18
18
|
import { loadRepoConfig, type RepoConfig } from '../repo-config.ts'
|
|
19
19
|
import { reportToMotherhome } from './motherhome.ts'
|
|
20
|
-
import {
|
|
20
|
+
import { unknownOffIds } from './when.ts'
|
|
21
21
|
import { unknownBudgetIds } from './budget.ts'
|
|
22
22
|
import { formatLine, type Line } from './wire.ts'
|
|
23
23
|
import { writeStatus } from './write-status.ts'
|
|
@@ -75,6 +75,8 @@ export type CheckOpts = {
|
|
|
75
75
|
buildOnly?: boolean
|
|
76
76
|
/** the gate asks the proactive checks too (`release.requireHealthy`) */
|
|
77
77
|
includeProactive?: boolean
|
|
78
|
+
/** the release preflight: the rows declaring `onRelease` run */
|
|
79
|
+
release?: boolean
|
|
78
80
|
/** ids this caller does not need (a sub-unit release) */
|
|
79
81
|
exclude?: string[]
|
|
80
82
|
/** the already-composed check list, when the caller built one (runCi does,
|
|
@@ -87,11 +89,8 @@ export type CheckOpts = {
|
|
|
87
89
|
/** the gate binary that own ledger is keyed on (default: the one running
|
|
88
90
|
* it). Ignored when `ledger` is passed, which carries its own. */
|
|
89
91
|
cliEntry?: string
|
|
90
|
-
/**
|
|
91
|
-
*
|
|
92
|
-
* `wiki/`, so writing it before the post-run re-hash makes the gate look
|
|
93
|
-
* like an agent editing the tree and throws away every result the run just
|
|
94
|
-
* measured. */
|
|
92
|
+
/** file the motherhome row in the machine db (core/motherhome.ts). runCi
|
|
93
|
+
* passes false and does it ITSELF, once, after its verdict. */
|
|
95
94
|
reportHome?: boolean
|
|
96
95
|
}
|
|
97
96
|
|
|
@@ -110,9 +109,9 @@ export type CheckRun = {
|
|
|
110
109
|
advisoryFailures: string[]
|
|
111
110
|
/** checks that never declared `blocking` — always fatal */
|
|
112
111
|
undeclared: string[]
|
|
113
|
-
/** `
|
|
112
|
+
/** `off` entries naming no check of this repo — a toggle that does
|
|
114
113
|
* nothing is worse than none, so it is fatal too */
|
|
115
|
-
|
|
114
|
+
unknownOff: string[]
|
|
116
115
|
/** `budgets` entries naming no check of this repo, fatal for the same reason */
|
|
117
116
|
unknownBudgets: string[]
|
|
118
117
|
/** what every asked-for check did, in declaration order */
|
|
@@ -128,6 +127,7 @@ function selectionOf(checks: RunnableCheck[], o: CheckOpts, tier: TestTier) {
|
|
|
128
127
|
checks,
|
|
129
128
|
audience: o.audience ?? 'health',
|
|
130
129
|
fullTier: tier === 'full',
|
|
130
|
+
release: o.release,
|
|
131
131
|
skipGroups: o.skipGroups,
|
|
132
132
|
onlyIds: o.only,
|
|
133
133
|
withIds: o.withIds,
|
|
@@ -214,7 +214,7 @@ export async function runCheckPass(root: string, o: CheckOpts = {}): Promise<Che
|
|
|
214
214
|
// page just replaced
|
|
215
215
|
if (summary.parked.length > 0) {
|
|
216
216
|
const head = summary.parked.slice(0, 8).join(', ')
|
|
217
|
-
say({ kind: 'skip', text: `${summary.parked.length} check(s) not in this run (
|
|
217
|
+
say({ kind: 'skip', text: `${summary.parked.length} check(s) not in this run (their runs do not name this context, or a skipped group, shipkit when shows each): ${head}${summary.parked.length > 8 ? `, and ${summary.parked.length - 8} more` : ''}` }, o.detail ?? print)
|
|
218
218
|
}
|
|
219
219
|
// the survey dies with the run that made it, and only the run that made it
|
|
220
220
|
if (inherited == null || inherited === '') for (const f of [sharePath, `${sharePath}.lock`]) { try { rmSync(f, { force: true }) } catch {} }
|
|
@@ -228,17 +228,17 @@ export async function runCheckPass(root: string, o: CheckOpts = {}): Promise<Che
|
|
|
228
228
|
if (o.ledger === undefined && ledger != null) for (const l of ledger.header(ledger.commit())) say(l)
|
|
229
229
|
// the caller that OPENED the record owns writing it, the same rule
|
|
230
230
|
if (o.runLog === undefined && log != null) for (const l of log.flush()) say(l)
|
|
231
|
-
//
|
|
231
|
+
// an `off` id naming nothing is a switch wired to no check: the id is read
|
|
232
232
|
// against the WHOLE inventory this run composed, never its narrowed list
|
|
233
233
|
const rows = composed.map((g) => g.row)
|
|
234
|
-
const
|
|
234
|
+
const unknownOff = unknownOffIds(rows, c)
|
|
235
235
|
const unknownBudgets = unknownBudgetIds(rows.map((r) => r.decl.id), c.budgets)
|
|
236
236
|
return {
|
|
237
237
|
lines: [...collected, ...lines],
|
|
238
238
|
blocked: !summary.ok,
|
|
239
239
|
advisoryFailures: summary.advisoryFailures,
|
|
240
240
|
undeclared: summary.undeclared,
|
|
241
|
-
|
|
241
|
+
unknownOff,
|
|
242
242
|
unknownBudgets,
|
|
243
243
|
results: summary.results,
|
|
244
244
|
interrupted: summary.interrupted,
|
|
@@ -250,13 +250,13 @@ export async function runCheck(root: string, o: CheckOpts = {}): Promise<number>
|
|
|
250
250
|
const print = o.print ?? ((l: string) => console.log(l))
|
|
251
251
|
const c = o.config ?? (await loadRepoConfig(root))
|
|
252
252
|
const run = await runCheckPass(root, { ...o, config: c })
|
|
253
|
-
const code = run.blocked || run.undeclared.length > 0 || run.
|
|
253
|
+
const code = run.blocked || run.undeclared.length > 0 || run.unknownOff.length > 0 || run.unknownBudgets.length > 0 ? 1 : 0
|
|
254
254
|
if (o.json === true) { print(JSON.stringify(run)); return code }
|
|
255
255
|
for (const u of run.undeclared) {
|
|
256
256
|
print(`FAIL: ${u} does not declare whether it blocks`)
|
|
257
257
|
print(` fix: add \`blocking: true\` (about this repo) or \`false\` (about the world) to that check`)
|
|
258
258
|
}
|
|
259
|
-
for (const key of [...run.
|
|
259
|
+
for (const key of [...run.unknownOff.map((id) => `off.${id}`), ...run.unknownBudgets.map((id) => `budgets.${id}`)]) {
|
|
260
260
|
print(`FAIL: ${key} names no check of this repo`)
|
|
261
261
|
print(` fix: \`shipkit checks\` lists every id; drop the entry or correct it`)
|
|
262
262
|
}
|
package/src/core/run-ci.ts
CHANGED
|
@@ -32,8 +32,11 @@ export type CiOpts = {
|
|
|
32
32
|
/** repair the mechanical slots before gating. default on locally, verify
|
|
33
33
|
* mode under CI. core/run-fixers.ts owns the scope. */
|
|
34
34
|
fix?: boolean
|
|
35
|
-
/** also apply the
|
|
35
|
+
/** also apply the repairs of the checks declaring `fix: 'ci'` during the
|
|
36
|
+
* fix pass */
|
|
36
37
|
fixRules?: boolean
|
|
38
|
+
/** the release preflight: the rows declaring `onRelease` run */
|
|
39
|
+
release?: boolean
|
|
37
40
|
/** self-arm the git hooks in the fix pass (interactive ci entry only) */
|
|
38
41
|
armHooks?: boolean
|
|
39
42
|
/** run only the checks that can block (core/run-checks onlyBlocking) */
|
|
@@ -203,6 +206,7 @@ export async function runCi(root: string, o: CiOpts = {}): Promise<number> {
|
|
|
203
206
|
composed,
|
|
204
207
|
audience: 'gate',
|
|
205
208
|
includeProactive: o.failOnAdvisory === true,
|
|
209
|
+
release: o.release,
|
|
206
210
|
frozen: o.frozen,
|
|
207
211
|
onlyBlocking: o.onlyBlockingChecks,
|
|
208
212
|
only: o.onlyIds,
|
|
@@ -226,7 +230,7 @@ export async function runCi(root: string, o: CiOpts = {}): Promise<number> {
|
|
|
226
230
|
})
|
|
227
231
|
const reg = ledger.layer.registration
|
|
228
232
|
for (const e of reg.errors) { registrationFailed = true; sink(`FAIL: ${e}`) }
|
|
229
|
-
if (registrationFailed) sink(' fix:
|
|
233
|
+
if (registrationFailed) sink(' fix: each check declares one cache that its body can honour: files, reads, ttl, fact or never (build/shipkit/runs.md)')
|
|
230
234
|
// both counts, always. Printing only the burn-down made `never:` the
|
|
231
235
|
// cheap way to shrink the visible number, and that field carries no
|
|
232
236
|
// verification at all: a growing decided population has to be as
|
|
@@ -262,7 +266,7 @@ export async function runCi(root: string, o: CiOpts = {}): Promise<number> {
|
|
|
262
266
|
}
|
|
263
267
|
// a switch wired to no check does nothing, silently, which is the one thing
|
|
264
268
|
// a toggle must never do
|
|
265
|
-
for (const key of [...run.
|
|
269
|
+
for (const key of [...run.unknownOff.map((id) => `off.${id}`), ...run.unknownBudgets.map((id) => `budgets.${id}`)]) {
|
|
266
270
|
sink(`FAIL: ${key} names no check of this repo`)
|
|
267
271
|
sink(` fix: \`shipkit checks\` lists every id; drop the entry or correct it`)
|
|
268
272
|
}
|
|
@@ -283,15 +287,15 @@ export async function runCi(root: string, o: CiOpts = {}): Promise<number> {
|
|
|
283
287
|
if (ledger != null) for (const [i, l] of ledger.header(ledger.commit()).entries()) (i === 0 ? detail : sink)(formatLine(l, false))
|
|
284
288
|
// one write for the whole gate, after everything has finished
|
|
285
289
|
for (const l of runLog.flush()) sink(formatLine(l, false))
|
|
286
|
-
const code = run.blocked || fixFailed || registrationFailed || advisoryFatal || run.undeclared.length > 0 || run.
|
|
290
|
+
const code = run.blocked || fixFailed || registrationFailed || advisoryFatal || run.undeclared.length > 0 || run.unknownOff.length > 0 || run.unknownBudgets.length > 0 ? 1 : 0
|
|
287
291
|
// THE VERDICT, one line and only on a pass: a red run already said so in
|
|
288
292
|
// its FAIL lines. The whole gate adds the tier and the cache counts, which
|
|
289
293
|
// a narrowed run asked no question about
|
|
290
294
|
const verdict = code === 0 && !run.interrupted ? verdictLine(run.results, scope.fullGate ? { tier, cache: ledger?.counts ?? null } : null) : null
|
|
291
295
|
if (verdict != null) sink(verdict)
|
|
292
|
-
// motherhome row
|
|
293
|
-
//
|
|
294
|
-
//
|
|
296
|
+
// the motherhome row, filed in the machine db and landed in the docsBase
|
|
297
|
+
// page by the daemon (core/motherhome.ts): never affects the gate, never
|
|
298
|
+
// writes in another repo, and a frozen run files nothing
|
|
295
299
|
if (o.frozen !== true) {
|
|
296
300
|
const mh = reportToMotherhome(root, c)
|
|
297
301
|
if (mh.note != null) print(mh.note)
|
package/src/core/run-fix.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
// `shipkit check --fix [<id> …]` — apply check
|
|
2
|
-
// show the real end state. Never
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// `shipkit check --fix [<id> …]` — apply check repairs, then re-run and
|
|
2
|
+
// show the real end state. Never caches (a repair must see reality). No ids =
|
|
3
|
+
// every check declaring `fix: 'ci'`, which is also what the gate's fix pass
|
|
4
|
+
// applies; a `fix: 'manual'` repair runs only when its id is named. A
|
|
5
|
+
// requested id that is unknown or has no repair fails loud instead of
|
|
6
|
+
// silently narrowing.
|
|
6
7
|
import { CheckCtx } from './check-ctx.ts'
|
|
7
8
|
import { formatLine } from './wire.ts'
|
|
8
|
-
import {
|
|
9
|
+
import type { FixWhen } from './runs.ts'
|
|
10
|
+
import { gateInventory } from '../ledger/gate-checks.ts'
|
|
9
11
|
import { loadRepoConfig } from '../repo-config.ts'
|
|
10
12
|
import { discoverChecks } from './discover-checks.ts'
|
|
11
13
|
import { runCheckBody, type CheckDef, type CheckEnv } from '../check.ts'
|
|
@@ -34,19 +36,25 @@ export async function runFix(root: string, o: FixOpts = {}): Promise<number> {
|
|
|
34
36
|
const ctx = (c: RepoConfig): CheckCtx => CheckCtx.open({ root, config: c })
|
|
35
37
|
const color = process.stdout.isTTY && process.env.NO_COLOR == null
|
|
36
38
|
/** one fixable check, whatever declared it */
|
|
37
|
-
type Target = { id: string; title: string; owner: string; run: CheckDef['run']; fix?: CheckDef['
|
|
39
|
+
type Target = { id: string; title: string; owner: string; run: CheckDef['run']; fix?: CheckDef['repair']; when: FixWhen; off: string | null }
|
|
40
|
+
// what each row resolved to: its declared `fix` and whether it is off
|
|
41
|
+
const resolved = new Map((await gateInventory(root, config)).map((r) => [r.decl.id, r.runs]))
|
|
38
42
|
// a rule the repo still registers fixes exactly like a check file, one
|
|
39
|
-
// code path
|
|
43
|
+
// code path, with the fix word its derived row carries
|
|
40
44
|
const all: Target[] = [
|
|
41
|
-
...(await discoverChecks(root)).map((u): Target => ({ id: u.id, title: u.title, owner: u.owner, run: u.def.run, fix: u.def.
|
|
42
|
-
...(config.rules ?? []).map((r): Target =>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
...(await discoverChecks(root)).map((u): Target => ({ id: u.id, title: u.title, owner: u.owner, run: u.def.run, fix: u.def.repair, when: resolved.get(u.id)?.fix ?? u.def.fix, off: resolved.get(u.id)?.off ?? null })),
|
|
46
|
+
...(config.rules ?? []).map((r): Target => {
|
|
47
|
+
const runs = resolved.get(r.id)
|
|
48
|
+
return {
|
|
49
|
+
id: r.id, title: r.title, owner: r.owner, when: runs?.fix ?? 'none', off: runs?.off ?? null,
|
|
50
|
+
run: (c) => r.lint(c, { root: c.root, config: c.config }),
|
|
51
|
+
fix: r.fix == null ? undefined : (c) => r.fix!({ root: c.root, config: c.config }),
|
|
52
|
+
}
|
|
53
|
+
}),
|
|
47
54
|
]
|
|
48
55
|
|
|
49
|
-
|
|
56
|
+
// no ids: the repairs declared for the gate. A manual one runs by name
|
|
57
|
+
let targets = all.filter((r) => r.fix != null && r.when === 'ci')
|
|
50
58
|
if (o.ids != null && o.ids.length > 0) {
|
|
51
59
|
for (const id of o.ids) {
|
|
52
60
|
const r = all.find((x) => x.id === id)
|
|
@@ -54,17 +62,16 @@ export async function runFix(root: string, o: FixOpts = {}): Promise<number> {
|
|
|
54
62
|
if (r.fix == null) { print(`FAIL: [${id}] has no autofix — fix by hand: ${config.docsBase ?? ''}${r.owner}`); return 1 }
|
|
55
63
|
}
|
|
56
64
|
const wanted = new Set(o.ids)
|
|
57
|
-
targets =
|
|
65
|
+
targets = all.filter((r) => r.fix != null && wanted.has(r.id))
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
let exit = 0
|
|
61
69
|
for (const rule of targets) {
|
|
62
70
|
const before = ctx(env.config)
|
|
63
|
-
|
|
64
|
-
await runCheckBody(rule, before, off.at === 'off' ? { reason: off.reason } : undefined)
|
|
71
|
+
await runCheckBody(rule, before, rule.off != null ? { reason: rule.off } : undefined)
|
|
65
72
|
// a check that is OFF never autofixes: the repo broke that invariant on purpose
|
|
66
|
-
if (off
|
|
67
|
-
if (o.quiet !== true) print(`skip: [${rule.id}] ${rule.id} exempt: ${off
|
|
73
|
+
if (rule.off != null) {
|
|
74
|
+
if (o.quiet !== true) print(`skip: [${rule.id}] ${rule.id} exempt: ${rule.off}`)
|
|
68
75
|
continue
|
|
69
76
|
}
|
|
70
77
|
if (!before.failed && !before.warned) {
|
|
@@ -97,8 +104,8 @@ export async function runFix(root: string, o: FixOpts = {}): Promise<number> {
|
|
|
97
104
|
for (const l of after.lines) (o.quiet === true ? o.detail ?? print : say)(formatLine({ ...l, text: l.text.replace(/ · autofix: shipkit check --fix \S+$/, '') }, color))
|
|
98
105
|
if (after.failed) exit = 1
|
|
99
106
|
}
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
102
|
-
print(`${
|
|
107
|
+
const manual = all.filter((r) => r.fix != null && r.when === 'manual').map((r) => r.id)
|
|
108
|
+
if (manual.length > 0 && o.quiet !== true && (o.ids == null || o.ids.length === 0))
|
|
109
|
+
print(`${manual.length} repair(s) run only by name: shipkit check --fix ${manual.join(' ')}`)
|
|
103
110
|
return exit
|
|
104
111
|
}
|
package/src/core/run-fixers.ts
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
// rewrites the tree it checks can turn red green with no commit. one
|
|
9
9
|
// mechanism, two modes, rather than a formatter plus a format:check that
|
|
10
10
|
// drift apart.
|
|
11
|
-
import { existsSync, readFileSync } from 'fs'
|
|
12
|
-
import { join } from 'path'
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
12
|
+
import { dirname, join } from 'path'
|
|
13
13
|
import { fileURLToPath } from 'url'
|
|
14
14
|
import { armHooks } from '../hooks/run-hooks.ts'
|
|
15
15
|
import { loadRepoConfig, type RepoConfig } from '../repo-config.ts'
|
|
@@ -22,6 +22,8 @@ import { layoutSummary, migrateLayout } from './migrate-layout.ts'
|
|
|
22
22
|
import { renderPolicy } from '../checks/SK004-gitignore-policy.check.ts'
|
|
23
23
|
import { spawnWithTimeout } from './spawn-timeout.ts'
|
|
24
24
|
import { gitBin } from './git-bin.ts'
|
|
25
|
+
import { LAYOUT } from './layout.ts'
|
|
26
|
+
import { gateBinary, toolchainIdentity } from '../ledger/toolchain-identity.ts'
|
|
25
27
|
|
|
26
28
|
const FIXER_TIMEOUT_MS = 120_000
|
|
27
29
|
|
|
@@ -123,6 +125,31 @@ function treeState(root: string): Map<string, string> | null {
|
|
|
123
125
|
return m
|
|
124
126
|
}
|
|
125
127
|
|
|
128
|
+
/** where the key of the last clean fix pass is kept, per checkout */
|
|
129
|
+
const memoPath = (root: string): string => join(root, LAYOUT.tmp, 'fix-pass.key')
|
|
130
|
+
|
|
131
|
+
/** what the fixers would see: HEAD, the dirty paths and their bytes, the
|
|
132
|
+
* toolchain that runs them, and their commands. null when HEAD cannot be read */
|
|
133
|
+
function fixMemo(root: string, tree: Map<string, string>, cmds: string[][]): string | null {
|
|
134
|
+
const head = Bun.spawnSync([gitBin(), 'rev-parse', 'HEAD'], { cwd: root })
|
|
135
|
+
if (head.exitCode !== 0) return null
|
|
136
|
+
const h = new Bun.CryptoHasher('sha256')
|
|
137
|
+
h.update(`head=${head.stdout.toString().trim()}\n`)
|
|
138
|
+
for (const [path, hash] of [...tree].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) h.update(`${path}\0${hash}\n`)
|
|
139
|
+
try { h.update(`toolchain=${toolchainIdentity(root, gateBinary()).id}\n`) } catch { return null }
|
|
140
|
+
h.update(`cmds=${JSON.stringify(cmds)}\n`)
|
|
141
|
+
return h.digest('hex')
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const readMemo = (root: string): string | null => { try { return readFileSync(memoPath(root), 'utf8').trim() } catch { return null } }
|
|
145
|
+
|
|
146
|
+
const writeMemo = (root: string, key: string): void => {
|
|
147
|
+
try {
|
|
148
|
+
mkdirSync(dirname(memoPath(root)), { recursive: true })
|
|
149
|
+
writeFileSync(memoPath(root), `${key}\n`)
|
|
150
|
+
} catch { /* a memo that cannot be kept only means the next pass runs the fixers */ }
|
|
151
|
+
}
|
|
152
|
+
|
|
126
153
|
/** the bytes of repo.config.ts and the set of check and check files, as one
|
|
127
154
|
* hash. Read without git, so a repo with no history is covered too */
|
|
128
155
|
function shapeStamp(root: string): string {
|
|
@@ -231,13 +258,22 @@ async function fixPassBody(root: string, loaded: RepoConfig, o: FixPassOpts, ver
|
|
|
231
258
|
for (const u of unfilled) detail(`skip: ${u.id} (${u.reason})`)
|
|
232
259
|
for (const d of disabled) detail(`skip: ${d.id} (${d.reason})`)
|
|
233
260
|
const before = treeState(root)
|
|
234
|
-
|
|
235
|
-
|
|
261
|
+
// the fixers already ran clean over exactly this tree (HEAD, every dirty
|
|
262
|
+
// path's bytes, the toolchain, the commands): running them again rewrites
|
|
263
|
+
// nothing and costs a whole formatter pass. CI never skips: there the
|
|
264
|
+
// pass IS the check
|
|
265
|
+
const memo = verify || before == null ? null : fixMemo(root, before, slots.map((s) => s.cmd))
|
|
266
|
+
const skip = memo != null && readMemo(root) === memo
|
|
267
|
+
let fixersOk = true
|
|
268
|
+
if (skip) detail(`ok: fixers skipped, the tree is unchanged since they last ran clean (${slots.length} fixer(s))`)
|
|
269
|
+
else for (const s of slots) {
|
|
236
270
|
const r = await spawnWithTimeout(s.cmd, { cwd: root, timeoutMs: FIXER_TIMEOUT_MS })
|
|
237
271
|
// a broken fixer must never be mistaken for a clean one: the gate
|
|
238
272
|
// still runs, but the failure is loud and names the command
|
|
239
|
-
if (r.code !== 0)
|
|
273
|
+
if (r.code !== 0) {
|
|
274
|
+
fixersOk = false
|
|
240
275
|
o.print(`WARN fix ${s.id} failed (exit ${r.code}) — ${r.out.trim().split('\n').at(-1)?.slice(0, 160) ?? ''}; the gate below still ran`)
|
|
276
|
+
}
|
|
241
277
|
}
|
|
242
278
|
// structural repo-config repair is never implicit, and never in verify mode
|
|
243
279
|
if (o.rules === true && !verify) await runFix(root, { quiet: true, print: o.print, detail: o.detail, config })
|
|
@@ -265,6 +301,7 @@ async function fixPassBody(root: string, loaded: RepoConfig, o: FixPassOpts, ver
|
|
|
265
301
|
return { ran: true, touched, failed: false, reshaped }
|
|
266
302
|
}
|
|
267
303
|
if (touched.length === 0) {
|
|
304
|
+
if (memo != null && !skip && fixersOk) writeMemo(root, memo)
|
|
268
305
|
detail(`ok: ${verify ? 'fixers verified' : 'fix pass'} clean (${slots.length} fixer(s), nothing to rewrite)`)
|
|
269
306
|
return { ran: true, touched, failed: false, reshaped }
|
|
270
307
|
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// WHAT RUNS WHEN, as data every surface prints the same way: the legend (each
|
|
2
|
+
// axis, each value, what it means) and one row of cells per check. Pure: it
|
|
3
|
+
// reads composed rows and returns strings, so `shipkit when`, the hub's when
|
|
4
|
+
// page and the docs page can never disagree.
|
|
5
|
+
import type { CheckRow } from '../ledger/gate-checks.ts'
|
|
6
|
+
import { cellText, RUNS_KEYS, type RunsAxis, type RunsSource } from './runs.ts'
|
|
7
|
+
|
|
8
|
+
export type MatrixAxis = RunsAxis | 'fix'
|
|
9
|
+
|
|
10
|
+
export const MATRIX_AXES: readonly MatrixAxis[] = [...RUNS_KEYS, 'fix']
|
|
11
|
+
|
|
12
|
+
/** one axis of the legend: the command or event it is, and every value */
|
|
13
|
+
export type LegendAxis = { axis: MatrixAxis | 'cache'; short: string; context: string; values: { value: string; means: string }[] }
|
|
14
|
+
|
|
15
|
+
/** THE legend. Every surface prints it verbatim, and build/shipkit/runs.md is
|
|
16
|
+
* generated from it */
|
|
17
|
+
export const RUNS_LEGEND: readonly LegendAxis[] = [
|
|
18
|
+
{
|
|
19
|
+
axis: 'onPreCommit', short: 'commit', context: '`git commit`, in the pre-commit hook, on the working tree',
|
|
20
|
+
values: [
|
|
21
|
+
{ value: 'false', means: 'never at commit' },
|
|
22
|
+
{ value: '{ budgetMs: n }', means: 'runs in process with no cache, before the message. A failure refuses the commit, a run past n ms (at most 100) warns' },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
axis: 'onCi', short: 'ci', context: '`shipkit ci`, the quick gate: an agent runs it once before it pushes its work, and the hub button runs a whole one. Named after the command, never a CI server',
|
|
27
|
+
values: [
|
|
28
|
+
{ value: 'true', means: 'in every quick gate: the cached answer while its key has not moved, else the body runs' },
|
|
29
|
+
{ value: 'false', means: 'not in the quick gate. `--with <id>` or `--only <id>` still runs it by name' },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
axis: 'onFull', short: 'full', context: '`shipkit ci --full`: the quick gate (every onCi row) plus every slow suite',
|
|
34
|
+
values: [
|
|
35
|
+
{ value: 'true', means: 'in the full gate' },
|
|
36
|
+
{ value: 'false', means: 'not in the full gate' },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
axis: 'onRelease', short: 'release', context: 'the preflight of `shipkit bump` and `shipkit release`: full tier, frozen, no cache read, no repair',
|
|
41
|
+
values: [
|
|
42
|
+
{ value: 'true', means: 'its body runs before a version ships, never from the cache, and a failure stops the release. A non blocking check runs there only when the repo sets release.requireHealthy' },
|
|
43
|
+
{ value: 'false', means: 'a release never asks it' },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
axis: 'onHealth', short: 'health', context: '`shipkit check`, the TUI checks panel and the hub health run: is this repo healthy',
|
|
48
|
+
values: [
|
|
49
|
+
{ value: 'true', means: 'part of the health pass' },
|
|
50
|
+
{ value: 'false', means: 'not part of it (every build step)' },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
axis: 'onSave', short: 'save', context: 'the daemon, 60s after the last save in the repo',
|
|
55
|
+
values: [
|
|
56
|
+
{ value: 'false', means: 'a save never starts it' },
|
|
57
|
+
{ value: "'moved'", means: 'runs only when a file it is keyed on moved' },
|
|
58
|
+
{ value: "'burst'", means: 'runs after every save burst, whatever moved (nothing keys it)' },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
axis: 'onClock', short: 'clock', context: 'the daemon, on its own time',
|
|
63
|
+
values: [
|
|
64
|
+
{ value: 'false', means: 'never on a clock' },
|
|
65
|
+
{ value: "'once'", means: 'once while no answer exists for its fact (first run, a changed declaration)' },
|
|
66
|
+
{ value: '{ every, inactive }', means: "on a period ('24h', a range ['20h','28h'], a cron, or a function of its last result). inactive 'wait' holds it while nobody works in the repo, 'run' runs it anyway" },
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
axis: 'fix', short: 'fix', context: 'which command applies the check\'s repair',
|
|
71
|
+
values: [
|
|
72
|
+
{ value: "'none'", means: 'no repair: a failure is fixed by hand' },
|
|
73
|
+
{ value: "'ci'", means: 'the fix pass of `shipkit ci` applies it after the body fails or warns, and `shipkit check --fix`' },
|
|
74
|
+
{ value: "'manual'", means: 'only `shipkit check --fix <id>` or the hub fix button' },
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
axis: 'cache', short: 'cache', context: 'how an answer is kept, in every context but the release and the hook',
|
|
79
|
+
values: [
|
|
80
|
+
{ value: 'files', means: 'keyed on these paths and the check\'s own code: the cached answer holds while they do not move' },
|
|
81
|
+
{ value: 'reads', means: 'keyed on what the body read last time, which the cache checks again before it answers' },
|
|
82
|
+
{ value: 'ttl', means: 'an answer about the world, kept this long' },
|
|
83
|
+
{ value: 'fact', means: 'a decided value, kept until the declaration changes' },
|
|
84
|
+
{ value: 'never', means: 'runs every time, with the reason' },
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
/** one cell: what it prints, whether the context runs it, and who said so */
|
|
90
|
+
export type RunsCell = { text: string; on: boolean; source: RunsSource }
|
|
91
|
+
|
|
92
|
+
/** one check as the matrix prints it */
|
|
93
|
+
export type RunsCells = {
|
|
94
|
+
cells: Record<MatrixAxis, RunsCell>
|
|
95
|
+
cache: string
|
|
96
|
+
/** the reason it runs nowhere, null for a live row */
|
|
97
|
+
off: string | null
|
|
98
|
+
/** why a derived row has its values, null for a declared one */
|
|
99
|
+
derivedWhy: string | null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function runsCellsOf(row: CheckRow): RunsCells {
|
|
103
|
+
const r = row.runs
|
|
104
|
+
const cells = Object.fromEntries(MATRIX_AXES.map((k) => {
|
|
105
|
+
const v = k === 'fix' ? r.fix : r[k]
|
|
106
|
+
const on = r.off == null && v !== false && v !== 'none'
|
|
107
|
+
return [k, { text: cellText(r, k), on, source: r.source[k] }]
|
|
108
|
+
})) as Record<MatrixAxis, RunsCell>
|
|
109
|
+
return { cells, cache: row.cache, off: r.off, derivedWhy: r.derivedWhy }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** where a row runs, as one line of words: every context that runs it, with
|
|
113
|
+
* its value when that says more than yes, then the command that repairs it */
|
|
114
|
+
export function runsWords(c: RunsCells): string {
|
|
115
|
+
if (c.off != null) return `runs nowhere: ${c.off}`
|
|
116
|
+
const word = (k: MatrixAxis): string => {
|
|
117
|
+
const cell = c.cells[k]
|
|
118
|
+
const short = RUNS_LEGEND.find((a) => a.axis === k)!.short
|
|
119
|
+
return `${short}${cell.text === 'yes' ? '' : ` ${cell.text}`}`
|
|
120
|
+
}
|
|
121
|
+
const on = RUNS_KEYS.filter((k) => c.cells[k].on).map(word)
|
|
122
|
+
const where = on.length === 0 ? 'runs only when named' : `runs on ${on.join(' · ')}`
|
|
123
|
+
return c.cells.fix.on ? `${where}, repaired by ${word('fix')}` : where
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** what a cell's value means, the legend's own sentence for it. A cell
|
|
127
|
+
* prints short (yes, ·, moved, 24h); the legend names the declared value */
|
|
128
|
+
export function cellMeaning(k: MatrixAxis | 'cache', text: string): string {
|
|
129
|
+
const values = RUNS_LEGEND.find((a) => a.axis === k)!.values
|
|
130
|
+
const declared = k === 'cache'
|
|
131
|
+
? text.split(/[\s:]/)[0]!
|
|
132
|
+
: text === 'yes' ? 'true'
|
|
133
|
+
: text === '·' ? 'false'
|
|
134
|
+
: k === 'onPreCommit' ? '{ budgetMs: n }'
|
|
135
|
+
: k === 'onClock' && text !== 'once' ? '{ every, inactive }'
|
|
136
|
+
: `'${text}'`
|
|
137
|
+
return values.find((v) => v.value === declared)?.means ?? ''
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** which rows a context runs, for the matrix's column filter */
|
|
141
|
+
export const inColumn = (c: RunsCells, k: MatrixAxis): boolean => c.cells[k].on
|
|
142
|
+
|
|
143
|
+
/** the matrix as terminal lines: the legend, then one row per check, and a
|
|
144
|
+
* row the repo turned off with its reason */
|
|
145
|
+
/** what a failure does, as one word: a build step and a blocking check stop
|
|
146
|
+
* you, a check about the world only warns */
|
|
147
|
+
export const stopsWord = (r: { blocking: boolean | null; build: boolean }): string => (r.build ? 'build' : r.blocking === false ? 'warns' : 'blocks')
|
|
148
|
+
|
|
149
|
+
export function matrixLines(rows: { id: string; blocking: boolean | null; build: boolean; runs: RunsCells }[], o: { legend: boolean; only?: MatrixAxis } = { legend: true }): string[] {
|
|
150
|
+
const out: string[] = []
|
|
151
|
+
if (o.legend) {
|
|
152
|
+
for (const a of RUNS_LEGEND) {
|
|
153
|
+
out.push(`${a.axis.padEnd(12)}${a.context}`)
|
|
154
|
+
for (const v of a.values) out.push(`${''.padEnd(14)}${v.value.padEnd(20)}${v.means}`)
|
|
155
|
+
}
|
|
156
|
+
out.push('')
|
|
157
|
+
}
|
|
158
|
+
const shown = o.only == null ? rows : rows.filter((r) => inColumn(r.runs, o.only!))
|
|
159
|
+
const width = Math.max(5, ...shown.map((r) => r.id.length)) + 2
|
|
160
|
+
const heads = MATRIX_AXES.map((k) => RUNS_LEGEND.find((a) => a.axis === k)!.short)
|
|
161
|
+
const colW = (i: number): number => Math.max(heads[i]!.length, ...shown.map((r) => cellOut(r.runs, MATRIX_AXES[i]!).length)) + 2
|
|
162
|
+
const widths = MATRIX_AXES.map((_, i) => colW(i))
|
|
163
|
+
out.push(`${'check'.padEnd(width)}${'fails'.padEnd(8)}${heads.map((h, i) => h.padEnd(widths[i]!)).join('')}cache`)
|
|
164
|
+
for (const r of shown) {
|
|
165
|
+
if (r.runs.off != null) { out.push(`${r.id.padEnd(width)}${stopsWord(r).padEnd(8)}off: ${r.runs.off}`); continue }
|
|
166
|
+
out.push(`${r.id.padEnd(width)}${stopsWord(r).padEnd(8)}${MATRIX_AXES.map((k, i) => cellOut(r.runs, k).padEnd(widths[i]!)).join('')}${r.runs.cache}`)
|
|
167
|
+
}
|
|
168
|
+
return out
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const cellOut = (c: RunsCells, k: MatrixAxis): string => c.cells[k].text
|
|
172
|
+
|
|
173
|
+
/** the legend as markdown, the block `shipkit when --out` writes into a docs
|
|
174
|
+
* page between its markers */
|
|
175
|
+
export function legendMarkdown(): string {
|
|
176
|
+
const out = ['| key | the context | value | what it means |', '|---|---|---|---|']
|
|
177
|
+
for (const a of RUNS_LEGEND) a.values.forEach((v, i) => out.push(`| ${i === 0 ? `\`${a.axis}\`` : ''} | ${i === 0 ? a.context.replace(/\|/g, '\\|') : ''} | \`${v.value}\` | ${v.means.replace(/\|/g, '\\|')} |`))
|
|
178
|
+
return out.join('\n')
|
|
179
|
+
}
|