rvlib-shipkit 1.4.0 → 1.6.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 +114 -0
- package/README.md +17 -17
- package/package.json +3 -2
- package/src/assets/gen/comfyts-gen.ts +12 -8
- package/src/assets/server/studio-server.ts +14 -2
- package/src/check.ts +73 -103
- package/src/checks/SK001-package-json-shape.check.ts +11 -6
- package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
- package/src/checks/SK003-claude-md-shape.check.ts +7 -3
- 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 +4 -2
- 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 +4 -0
- package/src/checks/SK015-nested-repo.check.ts +4 -2
- package/src/checks/SK016-legacy-check-shapes.check.ts +13 -12
- 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 +4 -1
- package/src/checks/SK028-readme.check.ts +5 -1
- package/src/checks/SK029-explicit-check-files.check.ts +95 -0
- package/src/checks/SK030-cache-audit.check.ts +34 -0
- package/src/checks/SK031-trace-audit.check.ts +39 -0
- package/src/checks/SK032-ignored-inputs.check.ts +79 -0
- package/src/cli/cmd_Asks.ts +5 -5
- package/src/cli/cmd_Assets.ts +17 -13
- package/src/cli/cmd_Bump.ts +12 -11
- package/src/cli/cmd_CacheAudit.ts +43 -0
- 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 +8 -6
- package/src/cli/cmd_Fleet.ts +9 -7
- package/src/cli/cmd_GlobalProcesses.ts +120 -7
- package/src/cli/cmd_Home.ts +26 -19
- package/src/cli/cmd_Hooks.ts +6 -5
- package/src/cli/cmd_Init.ts +5 -4
- package/src/cli/cmd_Keys.ts +20 -8
- package/src/cli/cmd_Mv.ts +7 -6
- package/src/cli/cmd_Needs.ts +4 -3
- package/src/cli/cmd_Overview.ts +5 -4
- 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 +6 -4
- 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_TraceAudit.ts +62 -0
- 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 +11 -2
- package/src/collections/checks.ts +2 -0
- package/src/collections/collection-types.ts +12 -4
- package/src/collections/providers.ts +5 -3
- package/src/core/app-icon.ts +154 -0
- package/src/core/audit-report.ts +21 -0
- package/src/core/cache-audit.ts +72 -0
- package/src/core/canonical-scripts.ts +2 -2
- package/src/core/check-ctx.ts +14 -5
- package/src/core/check-runner.ts +43 -23
- package/src/core/config-rewrite.ts +37 -24
- package/src/core/discover-checks.ts +23 -2
- package/src/core/floor-resolve.ts +5 -7
- package/src/core/git-root.ts +13 -0
- package/src/core/ignored-block.ts +94 -0
- package/src/core/ignored-inputs.ts +47 -0
- package/src/core/jobs.ts +9 -2
- package/src/core/legacy-shapes.ts +39 -10
- package/src/core/one-lib.ts +59 -4
- 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/read-set.ts +97 -9
- package/src/core/retention.ts +0 -14
- package/src/core/run-check.ts +12 -9
- package/src/core/run-ci.ts +14 -5
- package/src/core/run-fix.ts +30 -23
- package/src/core/run-fixers.ts +48 -5
- package/src/core/runs-matrix.ts +179 -0
- package/src/core/runs.ts +242 -0
- package/src/core/trace-audit.ts +120 -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 +10 -14
- package/src/fleet/scan-fleet.ts +20 -4
- package/src/fleet/update-fleet.ts +6 -6
- package/src/home/gui-app.ts +24 -27
- package/src/home/home-launchd.ts +20 -4
- package/src/home/home-routes.ts +12 -1
- package/src/home/home-start.ts +11 -5
- package/src/home/hubs.ts +5 -5
- package/src/home/open-app.ts +2 -2
- package/src/home/pm.ts +8 -0
- package/src/home/run-pm.ts +1 -0
- package/src/home/serve-web.ts +2 -2
- package/src/home/supervise.ts +24 -1
- package/src/home/via-hub.ts +14 -0
- package/src/hooks/run-hooks.ts +48 -4
- package/src/init-repo.ts +6 -6
- package/src/ledger/db.ts +2 -13
- package/src/ledger/gate-checks.ts +133 -61
- package/src/ledger/git-key.ts +153 -31
- package/src/ledger/git-scan.ts +5 -2
- package/src/ledger/ledger.ts +136 -21
- package/src/ledger/store.ts +14 -0
- package/src/ledger/toolchain-identity.ts +248 -45
- package/src/plan/repo-source.ts +18 -39
- package/src/problems/extract.ts +3 -0
- package/src/release/run-release.ts +5 -0
- package/src/repo-config.ts +46 -54
- 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 +1 -1
- package/src/serve/hub-app/admin-svc.ts +36 -3
- package/src/serve/hub-app/admin-view.tsx +46 -12
- 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 +14 -35
- package/src/serve/hub-app/col-card.tsx +1 -44
- package/src/serve/hub-app/col-tables-view.tsx +84 -11
- package/src/serve/hub-app/col-tables.ts +27 -0
- package/src/serve/hub-app/cols-svc.ts +5 -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 +6 -0
- 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 +14 -2
- package/src/serve/hub-app/home-strip.css +43 -12
- package/src/serve/hub-app/home-strip.tsx +115 -64
- package/src/serve/hub-app/home.html +1 -0
- package/src/serve/hub-app/hub-app.tsx +32 -8
- package/src/serve/hub-app/hub-types.ts +5 -2
- package/src/serve/hub-app/hub.css +89 -63
- package/src/serve/hub-app/icons.tsx +11 -0
- package/src/serve/hub-app/index.html +1 -0
- package/src/serve/hub-app/keys.ts +15 -4
- package/src/serve/hub-app/live-svc.ts +3 -2
- package/src/serve/hub-app/machine-st.ts +18 -22
- 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 +45 -0
- package/src/serve/hub-app/project-list.css +4 -1
- package/src/serve/hub-app/project-list.tsx +6 -3
- package/src/serve/hub-app/projects-svc.ts +25 -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 +131 -0
- package/src/serve/hub-app/repo-st.ts +70 -14
- package/src/serve/hub-app/sidebar.tsx +17 -42
- package/src/serve/hub-app/timeline-svc.ts +3 -2
- package/src/serve/hub-app/tree-build.ts +151 -155
- package/src/serve/hub-app/tree-row.tsx +55 -12
- package/src/serve/hub-app/tree-svc.ts +97 -56
- 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 +7 -18
- package/src/serve/project-gutter-route.ts +22 -1
- package/src/serve/run-serve.ts +1 -1
- package/src/serve/serve-hub.ts +4 -14
- package/src/suite/judge-suites.ts +6 -0
- package/src/trace/interpose.c +655 -0
- package/src/trace/read-trace.ts +485 -0
- package/src/trace/tracer.ts +282 -0
- 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/repo-config.ts
CHANGED
|
@@ -59,29 +59,11 @@ export type ServeConfig = {
|
|
|
59
59
|
* prints beside localhost and rides on every ping message, because a
|
|
60
60
|
* phone cannot resolve localhost */
|
|
61
61
|
publicHost?: string
|
|
62
|
-
/** the long running processes this repo needs (a dev server, a watcher).
|
|
63
|
-
* The hub supervises them: a switch each on its page, a crash restarts,
|
|
64
|
-
* and they outlive a hub restart */
|
|
65
|
-
processes?: ProcessDecl[]
|
|
66
62
|
/** the ids of the global processes this repo needs (any house repo's
|
|
67
63
|
* `globalProcess` unit files). The home rail marks the repo while one is off */
|
|
68
64
|
uses?: string[]
|
|
69
65
|
}
|
|
70
66
|
|
|
71
|
-
/** one long running process a repo needs, supervised by its hub */
|
|
72
|
-
export type ProcessDecl = {
|
|
73
|
-
/** kebab-case, unique in this repo */
|
|
74
|
-
id: string
|
|
75
|
-
/** the command as a list of words, run from the repo root */
|
|
76
|
-
cmd: string[]
|
|
77
|
-
/** where it runs, repo-relative (default the repo root) */
|
|
78
|
-
cwd?: string
|
|
79
|
-
/** the url it serves, probed for up and shown as a link */
|
|
80
|
-
url?: string
|
|
81
|
-
/** starts with the hub. Default false: it starts from its switch */
|
|
82
|
-
auto?: boolean
|
|
83
|
-
}
|
|
84
|
-
|
|
85
67
|
export type RepoConfig = {
|
|
86
68
|
/** the repo name, as every surface prints it: the gate header, the hub
|
|
87
69
|
* title, the motherhome row, the default hub port hash. The pre-commit
|
|
@@ -161,6 +143,15 @@ export type RepoConfig = {
|
|
|
161
143
|
/** repo-contributed .gitignore lines, rendered INSIDE rule SK004's managed
|
|
162
144
|
* block — for ecosystem-specific names the published lib may not carry */
|
|
163
145
|
gitignore?: string[]
|
|
146
|
+
/** gitignored paths a task READS, by task id (`'*'` for every task),
|
|
147
|
+
* repo-relative: a path joins that task's key by content (generated
|
|
148
|
+
* types, a local fixture), and `dir/**` by the names under the dir (an
|
|
149
|
+
* output folder a suite lists). A task keyed on the whole work tree sees
|
|
150
|
+
* no gitignored file, and a declared list names none either, so both
|
|
151
|
+
* carry these. The traced audit (SK031) finds the missing ones and the
|
|
152
|
+
* fix pass writes them into their own managed block; an entry written
|
|
153
|
+
* outside it stops that repair */
|
|
154
|
+
ignoredInputs?: Record<string, string[]>
|
|
164
155
|
/** untrack the paths shipkit generates on every run (STATUS.md and
|
|
165
156
|
* friends) the first time it sees them tracked, in one commit. Default
|
|
166
157
|
* ON: a generated file left in the index dirties every tree forever and
|
|
@@ -171,21 +162,25 @@ export type RepoConfig = {
|
|
|
171
162
|
* folder only: it never commits, untracks, tags or pushes on the parent's
|
|
172
163
|
* history. Rule SK015 fails when this and the disk disagree (core/nesting.ts) */
|
|
173
164
|
nested?: boolean
|
|
174
|
-
/**
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* `
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
|
|
165
|
+
/** THE CHECKS THIS REPO TURNS OFF, check id → why. The one thing a repo
|
|
166
|
+
* says about a check it does not own: where a check runs is its own
|
|
167
|
+
* declaration (build/shipkit/runs.md), and an off check runs in no
|
|
168
|
+
* context. `defineRepo` throws on a blank reason and on a rule id no
|
|
169
|
+
* check carries. The hub writes this map through the managed block in
|
|
170
|
+
* this file (core/when-block.ts). */
|
|
171
|
+
off?: Record<string, string>
|
|
172
|
+
/** RETIRED, read only while SK029 ports it into `off`: an `'off:<why>'`
|
|
173
|
+
* value reads as `off`, and any other word is refused, because a repo
|
|
174
|
+
* no longer places a check. Dies once `shipkit fleet` shows no repo
|
|
175
|
+
* carrying it */
|
|
181
176
|
when?: Record<string, string>
|
|
182
177
|
/** WHAT A HEALTHY RUN OF EACH CHECK TAKES, in ms, keyed by check id. A
|
|
183
178
|
* check that passes over its budget prints a WARN and reads yellow, and
|
|
184
179
|
* never blocks. It wins over the check's own `budgetMs` and the default
|
|
185
180
|
* for its kind (core/budget.ts). An id no check carries fails the run,
|
|
186
|
-
* the same as
|
|
181
|
+
* the same as an `off` entry. */
|
|
187
182
|
budgets?: Record<string, number>
|
|
188
|
-
/**
|
|
183
|
+
/** RETIRED, read as `off` while SK029 ports it there: rule id → the reason
|
|
189
184
|
* this repo breaks that invariant on purpose. An exempted rule does not
|
|
190
185
|
* lint and prints a skip line carrying the reason. `defineRepo` throws on
|
|
191
186
|
* an unknown id or a blank reason */
|
|
@@ -215,7 +210,7 @@ export type RepoConfig = {
|
|
|
215
210
|
/** widens the read-only tool allowlist SK008 enforces (default Read, Grep, Glob, Bash) */
|
|
216
211
|
tools?: string[]
|
|
217
212
|
}
|
|
218
|
-
/** ONE WAY IN escape hatch (build/shipkit.md): governed script names this
|
|
213
|
+
/** ONE WAY IN escape hatch (build/shipkit/one-way-in.md): governed script names this
|
|
219
214
|
* repo keeps as a direct tool call. Declared per NAME, never as a list of
|
|
220
215
|
* banned binaries: the invariant is about which VERBS an agent reaches
|
|
221
216
|
* for, and a binary blocklist goes stale the day a repo changes runner.
|
|
@@ -300,18 +295,7 @@ export type RepoConfig = {
|
|
|
300
295
|
logDir?: string
|
|
301
296
|
}
|
|
302
297
|
|
|
303
|
-
const KNOWN_KEYS = new Set(['name', 'venues', 'social', 'docsBase', 'defaults', 'tasks', 'asks', 'creds', 'credsCommand', 'credsReadCommand', 'notifyCommand', 'contracts', 'gitignore', 'untrackGenerated', 'nested', 'when', 'budgets', 'ruleExempt', 'visibility', 'archived', 'reviewer', 'oneWayIn', 'rules', 'release', 'units', 'notReleasable', 'hooks', 'checkDefaults', 'tui', 'serve', 'identity', 'logDir', 'prose', 'smoke', 'tests'])
|
|
304
|
-
|
|
305
|
-
const KEBAB = /^[a-z0-9][a-z0-9-]*$/
|
|
306
|
-
const words = (v: unknown): boolean => Array.isArray(v) && v.length > 0 && v.every((w) => typeof w === 'string')
|
|
307
|
-
|
|
308
|
-
/** why a process declaration cannot run, or null */
|
|
309
|
-
export function processProblem(d: ProcessDecl): string | null {
|
|
310
|
-
if (!KEBAB.test(d.id ?? '')) return `serve.processes: id '${String(d.id)}' must be kebab-case`
|
|
311
|
-
if (!words(d.cmd)) return `serve.processes['${d.id}'] needs cmd: a list of words`
|
|
312
|
-
if (d.url != null && !/^https?:\/\//.test(d.url)) return `serve.processes['${d.id}'].url must be an http url`
|
|
313
|
-
return null
|
|
314
|
-
}
|
|
298
|
+
const KNOWN_KEYS = new Set(['name', 'venues', 'social', 'docsBase', 'defaults', 'tasks', 'asks', 'creds', 'credsCommand', 'credsReadCommand', 'notifyCommand', 'contracts', 'gitignore', 'ignoredInputs', 'untrackGenerated', 'nested', 'off', 'when', 'budgets', 'ruleExempt', 'visibility', 'archived', 'reviewer', 'oneWayIn', 'rules', 'release', 'units', 'notReleasable', 'hooks', 'checkDefaults', 'tui', 'serve', 'identity', 'logDir', 'prose', 'smoke', 'tests'])
|
|
315
299
|
|
|
316
300
|
export function defineRepo(c: RepoConfig): RepoConfig {
|
|
317
301
|
if (typeof c.name !== 'string' || c.name === '') throw new Error('defineRepo: name is required')
|
|
@@ -321,12 +305,15 @@ export function defineRepo(c: RepoConfig): RepoConfig {
|
|
|
321
305
|
// a half-declared reviewer would lint a path like `.claude/agents/.md`
|
|
322
306
|
if (c.reviewer != null && (!/^[a-z0-9][a-z0-9-]*$/.test(c.reviewer.name ?? '') || (c.reviewer.brief ?? '') === ''))
|
|
323
307
|
throw new Error(`defineRepo: reviewer needs { name: kebab-case, brief: path to the standing brief }`)
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
308
|
+
if (c.serve != null && 'processes' in c.serve)
|
|
309
|
+
throw new Error(`defineRepo: serve.processes was removed; declare each process in a <name>.shipkit.ts file as \`export default globalProcess({...})\``)
|
|
310
|
+
if (c.ignoredInputs != null && (typeof c.ignoredInputs !== 'object' || Array.isArray(c.ignoredInputs)))
|
|
311
|
+
throw new Error(`defineRepo: ignoredInputs is { <task id or '*'>: [paths] }`)
|
|
312
|
+
for (const [task, paths] of Object.entries(c.ignoredInputs ?? {})) {
|
|
313
|
+
if (task === '' || !Array.isArray(paths)) throw new Error(`defineRepo: ignoredInputs '${task}' must name a task (or '*') and list paths`)
|
|
314
|
+
for (const p of paths)
|
|
315
|
+
if (typeof p !== 'string' || p === '' || p.startsWith('/') || p.split('/').includes('..'))
|
|
316
|
+
throw new Error(`defineRepo: ignoredInputs '${String(p)}' must be a repo-relative path inside the repo`)
|
|
330
317
|
}
|
|
331
318
|
// unknown keys die loud — a typo'd or removed key silently changing the
|
|
332
319
|
// gate is exactly the never-silence failure mode (A23)
|
|
@@ -384,17 +371,22 @@ export function defineRepo(c: RepoConfig): RepoConfig {
|
|
|
384
371
|
// repo rules are validated against the built-ins, so a bad id, a duplicate,
|
|
385
372
|
// or an SK squatter dies at config load, not silently mid-check
|
|
386
373
|
if (c.rules != null) validateRules(c.rules, { label: 'defineRepo: rules', taken: builtinCheckIds() })
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
|
|
374
|
+
// `off` silences a check, so it names one that exists and says why. The
|
|
375
|
+
// RULE namespace is the one this file can check: a task is resolved from
|
|
376
|
+
// the floor and a check is discovered on disk, so those two are caught by
|
|
377
|
+
// the gate against the real inventory (core/when.ts unknownOffIds)
|
|
378
|
+
for (const [id, reason] of Object.entries(c.off ?? {})) {
|
|
379
|
+
if (id.trim() === '') throw new Error(`defineRepo: off has an entry with no check id`)
|
|
380
|
+
if (typeof reason !== 'string' || reason.trim() === '') throw new Error(`defineRepo: off.${id} needs a reason: a check nobody can explain is a silenced gate`)
|
|
381
|
+
if (RULE_ID_RE.test(id) && !builtinCheckIds().includes(id) && !(c.rules ?? []).some((r) => r.id === id))
|
|
382
|
+
throw new Error(`defineRepo: off.${id} names no rule, the ids are in \`shipkit checks\``)
|
|
383
|
+
}
|
|
384
|
+
// the retired `when`: only its `off:<why>` word still means something
|
|
392
385
|
for (const [id, raw] of Object.entries(c.when ?? {})) {
|
|
393
|
-
if (id.trim() === '') throw new Error(`defineRepo: when has an entry with no check id`)
|
|
394
386
|
const w = parseWhen(raw)
|
|
395
387
|
if (typeof w === 'string') throw new Error(`defineRepo: when.${id} ${w}`)
|
|
396
|
-
if (
|
|
397
|
-
throw new Error(`defineRepo: when.${id}
|
|
388
|
+
if (w.at !== 'off')
|
|
389
|
+
throw new Error(`defineRepo: when.${id} '${raw}' no longer places a check: every check declares where it runs (build/shipkit/runs.md). A repo can only turn one off: off: { ${id}: '<why>' }`)
|
|
398
390
|
}
|
|
399
391
|
// a budget turns a slow pass yellow, so a value it cannot compare with dies here
|
|
400
392
|
for (const [id, v] of Object.entries(c.budgets ?? {})) {
|
package/src/rules/rule.ts
CHANGED
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
//
|
|
7
7
|
// A new invariant is a `<name>.check.ts` file: `check({ blocking, run })`,
|
|
8
8
|
// dropped anywhere in the repo, discovered with no config edit.
|
|
9
|
-
import {
|
|
9
|
+
import { repoOff } from '../core/when.ts'
|
|
10
10
|
import { tagCheckLines, checkCacheExtra, type Rule, type RuleEnv } from '../check.ts'
|
|
11
11
|
|
|
12
12
|
export type { FixResult, Rule, RuleEnv } from '../check.ts'
|
|
13
13
|
|
|
14
14
|
/** the config part of a rule's cache identity, including where it runs */
|
|
15
15
|
export function ruleCacheExtra(rule: Rule, env: RuleEnv): string | undefined {
|
|
16
|
-
return checkCacheExtra(rule
|
|
16
|
+
return checkCacheExtra(rule.cacheKey?.(env), repoOff(env.config, rule.id)?.reason ?? null)
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/** tag every severity line with the rule id (check.ts owns the rendering) */
|
|
@@ -7,7 +7,7 @@ always-loaded one, and route the rest by surface.
|
|
|
7
7
|
## Gates
|
|
8
8
|
|
|
9
9
|
- **Spec-first**: the `agent/` page changes BEFORE the code it describes.
|
|
10
|
-
- **`
|
|
10
|
+
- **`sk ci` is the verification.** Run it, not the linter, the
|
|
11
11
|
typechecker and the tests one at a time.
|
|
12
12
|
- **Repro before fix**: write the failing test, then fix, then re-run it.
|
|
13
13
|
- One code path per concern. Delete the redundant mechanism rather than
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
## Quick start
|
|
6
6
|
|
|
7
7
|
- `bun run hub` → the repo toolbox at http://localhost:{{PORT}}
|
|
8
|
-
- `
|
|
8
|
+
- `sk ci` → THE gate: run it to check your work, never the parts
|
|
9
9
|
|
|
10
10
|
This folder is the project's internal wiki and its SPEC. Update the page
|
|
11
11
|
BEFORE the change it describes.
|
|
@@ -11,12 +11,12 @@ description: Prove a change in THIS project actually works — drive the real fl
|
|
|
11
11
|
|
|
12
12
|
## The rules (house-wide)
|
|
13
13
|
- Failing repro test BEFORE the fix. Green checks ≠ verification.
|
|
14
|
-
- Logic → fast headless bun tests, run through `
|
|
14
|
+
- Logic → fast headless bun tests, run through `sk ci --only test` (tiny ~10-line test files reusing fixtures). While you fix one bug, `bun run test:raw <test file>` is the narrow way in, and one test by name is `bun run test:raw -- -t "<name>"`.
|
|
15
15
|
- Look & feel → NEVER screenshot-self-verify; the repo owner playtests it.
|
|
16
16
|
- Verify as the end user would: drive the real flow, observe the real signal.
|
|
17
17
|
|
|
18
18
|
## This project
|
|
19
|
-
- Full gate: `
|
|
19
|
+
- Full gate: `sk ci` — THE command, never the linter, the typechecker
|
|
20
20
|
and the tests one at a time.
|
|
21
21
|
- TODO, fill these in once the stack exists: the dev server command and its
|
|
22
22
|
port (check it is not already running: `curl -s localhost:PORT >/dev/null
|
|
@@ -9,6 +9,7 @@ import { clampCap, DEFAULT_CAP, DEFAULT_MINUTES, idleRow, WINDOW_MINUTES, type A
|
|
|
9
9
|
// the words the page and `shipkit cross-repo-activity` share live beside the shapes
|
|
10
10
|
export { ageShort, CAP_MAX, CAP_MIN, clampCap, DEFAULT_CAP, dirtWords, filesWords, upstreamWords, windowWords } from '../../home/activity-shape.ts'
|
|
11
11
|
import type { HomeSt } from './home-st.ts'
|
|
12
|
+
import { ownTrouble, reach } from './reach.ts'
|
|
12
13
|
|
|
13
14
|
export const ACTIVITY_POLL_MS = 60_000
|
|
14
15
|
const MINUTES_KEY = 'home:activity:minutes'
|
|
@@ -105,11 +106,11 @@ export class ActivitySt {
|
|
|
105
106
|
/** the repo in Fork, the git client: the daemon opens it by name */
|
|
106
107
|
async openFork(name: string): Promise<void> {
|
|
107
108
|
try {
|
|
108
|
-
const r = await
|
|
109
|
+
const r = await reach('/api/projects/fork', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name }) })
|
|
109
110
|
if (!r.ok) throw new Error(await r.text())
|
|
110
111
|
runInAction(() => { this.error = '' })
|
|
111
112
|
} catch (e) {
|
|
112
|
-
runInAction(() => { this.error = `open ${name} in Fork: ${
|
|
113
|
+
runInAction(() => { this.error = ownTrouble(e, this.st.machine, (m) => `open ${name} in Fork: ${m}`) })
|
|
113
114
|
}
|
|
114
115
|
}
|
|
115
116
|
|
|
@@ -128,12 +129,12 @@ export class ActivitySt {
|
|
|
128
129
|
const id = ++this.gen
|
|
129
130
|
runInAction(() => { this.loading = true; this.nextAt = Date.now() + ACTIVITY_POLL_MS })
|
|
130
131
|
try {
|
|
131
|
-
const r = await
|
|
132
|
+
const r = await reach(`/api/home/activity.json?m=${this.minutes}${fresh ? '&fresh=1' : ''}`, { signal: AbortSignal.timeout(30_000) })
|
|
132
133
|
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`)
|
|
133
134
|
const j = (await r.json()) as ActivityAnswer
|
|
134
135
|
runInAction(() => { if (id === this.gen) { this.data = j; this.error = '' } })
|
|
135
136
|
} catch (e) {
|
|
136
|
-
runInAction(() => { if (id === this.gen) this.error = (e
|
|
137
|
+
runInAction(() => { if (id === this.gen) this.error = ownTrouble(e, this.st.machine) })
|
|
137
138
|
} finally {
|
|
138
139
|
// the countdown starts when the answer lands, so reads never overlap
|
|
139
140
|
runInAction(() => { if (id === this.gen) { this.loading = false; this.nextAt = Date.now() + ACTIVITY_POLL_MS } })
|
|
@@ -52,7 +52,7 @@ const Next = observer(({ app }: { app: HomeSt }) => {
|
|
|
52
52
|
})
|
|
53
53
|
|
|
54
54
|
/** one commit's cells; `old` is the last one, from before the window */
|
|
55
|
-
const Commit = ({ c, now, old }: { c: ActivityCommit; now: number; old: boolean }) => (
|
|
55
|
+
export const Commit = ({ c, now, old }: { c: ActivityCommit; now: number; old: boolean }) => (
|
|
56
56
|
<>
|
|
57
57
|
<td className="acsha">{c.sha}</td>
|
|
58
58
|
<td className="acwhen" title={stampWords(c.at)}>{ageShort(c.at, now)}</td>
|
|
@@ -1,17 +1,50 @@
|
|
|
1
1
|
// A repo's admin page, as state: what administers the repo rather than works
|
|
2
|
-
// in it. Archive asks first (the confirmation is this state, so it survives a
|
|
2
|
+
// in it. Its identity colours, with the app icon they give it. Archive asks first (the confirmation is this state, so it survives a
|
|
3
3
|
// re-render and a test drives it headless), reload and keep active go to the
|
|
4
4
|
// machine by the repo's key. On a hub of its own there is no machine, and no
|
|
5
5
|
// admin page.
|
|
6
|
-
import { makeAutoObservable } from 'mobx'
|
|
6
|
+
import { makeAutoObservable, runInAction } from 'mobx'
|
|
7
|
+
import type { Identity } from '../../core/identity-color.ts'
|
|
7
8
|
import type { RepoSt } from './repo-st.ts'
|
|
8
9
|
|
|
10
|
+
/** how long a picker rests before the preview draws its colours */
|
|
11
|
+
export const PREVIEW_REST_MS = 150
|
|
12
|
+
|
|
9
13
|
export class AdminSvc {
|
|
10
14
|
/** the archive button was pressed: its confirmation shows in its place */
|
|
11
15
|
confirming = false
|
|
16
|
+
/** the colours the preview keeps while a picker moves, so a drag draws a
|
|
17
|
+
* few icons, not one per frame; null follows the repo's colours */
|
|
18
|
+
held: Identity | null = null
|
|
19
|
+
/** the repo has no png mark, or it would not draw: the app wears the default icon */
|
|
20
|
+
noIcon = false
|
|
21
|
+
private rest: ReturnType<typeof setTimeout> | null = null
|
|
12
22
|
|
|
13
23
|
constructor(private st: RepoSt) {
|
|
14
|
-
makeAutoObservable<this, 'st'>(this, { st: false })
|
|
24
|
+
makeAutoObservable<this, 'st' | 'rest'>(this, { st: false, rest: false })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** by key: the project list and the icon route know a repo by its
|
|
28
|
+
* folder, and its config can name it otherwise */
|
|
29
|
+
get identity(): Identity {
|
|
30
|
+
return this.st.home.projects.identityOf(this.st.key)
|
|
31
|
+
}
|
|
32
|
+
/** one colour, shown at once and saved to repo.config.ts once it rests */
|
|
33
|
+
setColour(patch: Partial<Identity>): void {
|
|
34
|
+
this.held ??= this.identity
|
|
35
|
+
this.st.home.projects.setIdentity(this.st.key, patch)
|
|
36
|
+
if (this.rest != null) clearTimeout(this.rest)
|
|
37
|
+
this.rest = setTimeout(() => runInAction(() => { this.held = null }), PREVIEW_REST_MS)
|
|
38
|
+
}
|
|
39
|
+
get previewSrc(): string {
|
|
40
|
+
const id = this.held ?? this.identity
|
|
41
|
+
return `/api/projects/app-icon/${encodeURIComponent(this.st.key)}?light=${encodeURIComponent(id.colorLight)}&dark=${encodeURIComponent(id.colorDark)}`
|
|
42
|
+
}
|
|
43
|
+
previewFailed(): void {
|
|
44
|
+
this.noIcon = true
|
|
45
|
+
}
|
|
46
|
+
previewLoaded(): void {
|
|
47
|
+
this.noIcon = false
|
|
15
48
|
}
|
|
16
49
|
|
|
17
50
|
askArchive(): void {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// THE ADMIN PAGE of a repo (`#page:admin`): what administers the repo rather
|
|
2
|
-
// than works in it
|
|
2
|
+
// than works in it. Its identity first (the colours, and the app icon they
|
|
3
|
+
// draw, core/app-icon.ts), then one row per control, each with the one sentence that says
|
|
3
4
|
// what it does. State in admin-svc.ts. Only on the home: a hub of its own has
|
|
4
5
|
// no machine to administer it.
|
|
5
6
|
import { observer } from 'mobx-react-lite'
|
|
@@ -16,6 +17,36 @@ const Row = ({ title, says, children }: { title: string; says: string; children:
|
|
|
16
17
|
</div>
|
|
17
18
|
)
|
|
18
19
|
|
|
20
|
+
/** the repo's two colours, and what they look like: its app icon and its name on both pages */
|
|
21
|
+
const IdentityBox = observer(({ app }: { app: RepoSt }) => {
|
|
22
|
+
const a = app.admin
|
|
23
|
+
const id = a.identity
|
|
24
|
+
const name = app.name
|
|
25
|
+
return (
|
|
26
|
+
<div className="adbox adid">
|
|
27
|
+
<div className="adicon">
|
|
28
|
+
<img src={a.previewSrc} alt={`${name} app icon`} width={128} height={128} hidden={a.noIcon}
|
|
29
|
+
onLoad={() => a.previewLoaded()} onError={() => a.previewFailed()} />
|
|
30
|
+
{a.noIcon ? <span className="dim">no png mark (.shipkit/identity/icon.png, public/logo.png or public/favicon.png): its app wears the default icon</span> : null}
|
|
31
|
+
</div>
|
|
32
|
+
<div className="adtext">
|
|
33
|
+
<b>identity</b>
|
|
34
|
+
<span className="dim">the repo's two colours, written to repo.config.ts as identity. The app icon sets the mark on the one it stands out on most, and each app redraws it at its next open.</span>
|
|
35
|
+
<label className="adpick">
|
|
36
|
+
<input type="color" value={id.colorDark} onChange={(e) => a.setColour({ colorDark: e.currentTarget.value })} />
|
|
37
|
+
<span className="adsample dark" style={{ color: id.colorDark }}>{name}</span>
|
|
38
|
+
<span className="dim">on a dark page, colorDark {id.colorDark}</span>
|
|
39
|
+
</label>
|
|
40
|
+
<label className="adpick">
|
|
41
|
+
<input type="color" value={id.colorLight} onChange={(e) => a.setColour({ colorLight: e.currentTarget.value })} />
|
|
42
|
+
<span className="adsample light" style={{ color: id.colorLight }}>{name}</span>
|
|
43
|
+
<span className="dim">on a light page, colorLight {id.colorLight}</span>
|
|
44
|
+
</label>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
|
|
19
50
|
export const AdminPage = observer(({ app }: { app: RepoSt }) => {
|
|
20
51
|
const a = app.admin
|
|
21
52
|
const w = app.home.machine?.worker(app.key)
|
|
@@ -26,17 +57,20 @@ export const AdminPage = observer(({ app }: { app: RepoSt }) => {
|
|
|
26
57
|
<span className="tid">admin</span>
|
|
27
58
|
<h3 className="ctitle">{name}</h3>
|
|
28
59
|
</div>
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
60
|
+
<IdentityBox app={app} />
|
|
61
|
+
<div className="adbox">
|
|
62
|
+
<Row title="reload the hub" says={`stops ${name}'s hub in the daemon and loads it again, its repo.config.ts read fresh and its processes started again. The daemon already does it on its own when repo.config.ts changes: this is for a hub that is stuck.${w == null ? '' : ` Its hub is ${w.status} now.`}`}>
|
|
63
|
+
<button className="ghost" onClick={() => void a.reload()}>reload</button>
|
|
64
|
+
</Row>
|
|
65
|
+
<Row title="keep active" says="an active repo runs the checks the plan holds for it and gets its red pings. It is active on its own while an agent works in it or VS Code has it open: this keeps it active without either.">
|
|
66
|
+
<label className="adflag"><input type="checkbox" checked={a.active} onChange={(e) => void a.setActive(e.currentTarget.checked)} />active</label>
|
|
67
|
+
</Row>
|
|
68
|
+
<Row title="archive" says="it leaves the project list for the archived page, and nothing runs there: no hub, no watch, no plan rows, no processes, no widgets. Written to repo.config.ts as archived: true, and undone from the archived page.">
|
|
69
|
+
{a.confirming
|
|
70
|
+
? <span className="adconfirm"><span>archive {name}?</span><button className="primary" onClick={() => a.archive()}>archive it</button><button className="ghost" onClick={() => a.cancel()}>cancel</button></span>
|
|
71
|
+
: <button className="ghost" onClick={() => a.askArchive()}>archive</button>}
|
|
72
|
+
</Row>
|
|
73
|
+
</div>
|
|
40
74
|
</div>
|
|
41
75
|
)
|
|
42
76
|
})
|
|
@@ -1,45 +1,42 @@
|
|
|
1
1
|
// ONE GATE CHECK, on its own page, as ONE compact block: the id as a tag, the
|
|
2
2
|
// title, the sentence it wrote about itself, the exact command it runs, then
|
|
3
3
|
// one grid of facts, each a dim label and a short value whose explanation is
|
|
4
|
-
// the row's `(?)
|
|
4
|
+
// the row's `(?)`, then where it runs: one line per context. No box holds a single field. Navigation is the tree on the
|
|
5
5
|
// left, so there is no back link.
|
|
6
6
|
import { observer } from 'mobx-react-lite'
|
|
7
7
|
import { useEffect, useRef } from 'react'
|
|
8
8
|
import { CACHED } from '../../core/outcome-words.ts'
|
|
9
|
+
import { cellMeaning, MATRIX_AXES, RUNS_LEGEND } from '../../core/runs-matrix.ts'
|
|
9
10
|
import { durationTone, fmtMs } from './dur.ts'
|
|
10
11
|
import { Help, TERMS } from './help.tsx'
|
|
11
12
|
import type { RepoSt } from './repo-st.ts'
|
|
12
|
-
import type { SlowSuite, CheckFacts, RecentRun
|
|
13
|
+
import type { SlowSuite, CheckFacts, RecentRun } from './hub-types.ts'
|
|
13
14
|
import { renderMd, renderMdInline } from './md.ts'
|
|
14
15
|
import { nextRun } from './next-run.ts'
|
|
15
16
|
import { ProseHits } from './prose-hits-view.tsx'
|
|
16
17
|
import { stampWords, TimeAgo, TimeAhead } from './time-ago.tsx'
|
|
17
|
-
import { checkName, commandOf, docPath, editorHref, lastRun, rememberedFact, repoPath, whatFacts,
|
|
18
|
+
import { checkName, commandOf, docPath, editorHref, lastRun, rememberedFact, repoPath, whatFacts, type ShortPath } from './check-words.ts'
|
|
18
19
|
|
|
19
20
|
/** which declaration is answering, so the switch never looks like it wrote
|
|
20
21
|
* something it did not. The CHIP is the short one, its help is the sentence */
|
|
21
22
|
const SOURCE_CHIP: Record<CheckFacts['whenSource'], string> = {
|
|
22
|
-
|
|
23
|
+
off: 'repo.config.ts',
|
|
24
|
+
when: 'when (retired)',
|
|
25
|
+
ruleExempt: 'ruleExempt (retired)',
|
|
23
26
|
'check-file': 'the check file',
|
|
24
|
-
ruleExempt: 'ruleExempt',
|
|
25
|
-
optIn: 'optIn',
|
|
26
|
-
'gate-export': 'gate = false',
|
|
27
27
|
defaults: 'defaults',
|
|
28
|
-
default: '
|
|
28
|
+
default: 'derived',
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const SOURCE_WORD: Record<CheckFacts['whenSource'], string> = {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
ruleExempt: '
|
|
35
|
-
|
|
36
|
-
'gate-export': 'from `export const gate = false` in its own file',
|
|
32
|
+
off: 'turned off in the `off` block of `repo.config.ts`, which this switch writes',
|
|
33
|
+
when: 'turned off by the retired `when` key, which `shipkit ci` ports to `off` (SK029)',
|
|
34
|
+
ruleExempt: 'turned off by the retired `ruleExempt` key, which `shipkit ci` ports to `off` (SK029)',
|
|
35
|
+
'check-file': 'declared by the check file itself: every context it runs in, no default',
|
|
37
36
|
defaults: 'from the `defaults` block, which removes the floor slot entirely',
|
|
38
|
-
default: '
|
|
37
|
+
default: 'a row with no file of its own: the code that derives it writes every context',
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
const WHEN_VALUES: WhenAt[] = ['always', 'release', 'health', 'off']
|
|
42
|
-
|
|
43
40
|
const VERDICT_WORD: Record<'pass' | 'warn' | 'fail' | 'skip', string> = {
|
|
44
41
|
pass: 'passed', warn: 'warned', fail: 'failed', skip: 'skipped',
|
|
45
42
|
}
|
|
@@ -126,44 +123,67 @@ export const CommandLine = ({ app, u }: { app: RepoSt; u: CheckFacts }) => {
|
|
|
126
123
|
)
|
|
127
124
|
}
|
|
128
125
|
|
|
129
|
-
// ──
|
|
126
|
+
// ── where it runs ────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
/** every context, one line each: the value the check declared and what it
|
|
129
|
+
* means in the legend's words. The cache closes it, the off switch ends it */
|
|
130
|
+
const WhereItRuns = observer(({ app, u }: { app: RepoSt; u: CheckFacts }) => {
|
|
131
|
+
const r = u.runs
|
|
132
|
+
return (
|
|
133
|
+
<Section title="where it runs" help="every context a check declares, with no default. The when page of this repo shows every check at once, and `sk when` prints the same">
|
|
134
|
+
{r.off != null
|
|
135
|
+
? <div className="wline">runs nowhere: {r.off}</div>
|
|
136
|
+
: <div className="cruns">
|
|
137
|
+
{MATRIX_AXES.map((k) => {
|
|
138
|
+
const c = r.cells[k]
|
|
139
|
+
const a = RUNS_LEGEND.find((x) => x.axis === k)!
|
|
140
|
+
return (
|
|
141
|
+
<div key={k} className={`crun${c.on ? ' on' : ''}`}>
|
|
142
|
+
<span className="ck" title={`${k}: ${a.context.replace(/`/g, '')}. From ${c.source}`}>{a.short}</span>
|
|
143
|
+
<span className="crv">{c.on ? c.text : 'no'}</span>
|
|
144
|
+
<span className="crm">{cellMeaning(k, c.text)}</span>
|
|
145
|
+
</div>
|
|
146
|
+
)
|
|
147
|
+
})}
|
|
148
|
+
<div className="crun cache">
|
|
149
|
+
<span className="ck" title={`cache: how an answer is kept. ${r.cache}`}>cache</span>
|
|
150
|
+
<span className="crv">{r.cache.split(/[\s:]/)[0]}</span>
|
|
151
|
+
<span className="crm">{cellMeaning('cache', r.cache)}</span>
|
|
152
|
+
</div>
|
|
153
|
+
</div>}
|
|
154
|
+
{r.derivedWhy == null ? null : <div className="note">no file declares this row: {r.derivedWhy}</div>}
|
|
155
|
+
<div className="cfacts"><OffSwitch app={app} u={u} /></div>
|
|
156
|
+
</Section>
|
|
157
|
+
)
|
|
158
|
+
})
|
|
130
159
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
*
|
|
135
|
-
|
|
160
|
+
// ── the off switch ───────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
/** the one thing this repo says about a check: that it is OFF here, with the
|
|
163
|
+
* reason. It writes `repo.config.ts` through the managed block and leaves the
|
|
164
|
+
* file dirty for its owner to review */
|
|
165
|
+
const OffSwitch = observer(({ app, u }: { app: RepoSt; u: CheckFacts }) => {
|
|
136
166
|
const s = app.checks
|
|
137
167
|
const g = s.gate
|
|
138
|
-
const shown = s.whenShown(u)
|
|
139
168
|
const reason = s.reasonFor(u.checkId)
|
|
169
|
+
const off = u.runs.off
|
|
140
170
|
return (
|
|
141
|
-
<Row k="
|
|
171
|
+
<Row k="off in this repo" wide help="where this check runs is its own declaration; the one thing this repo says is that it is OFF here, with why. The switch writes the `off` block of repo.config.ts and leaves the file for you to commit">
|
|
142
172
|
<span className="cwhen">
|
|
143
|
-
<
|
|
144
|
-
{WHEN_VALUES.map((v) => <option key={v} value={v}>{WHEN_OPTION[v]}</option>)}
|
|
145
|
-
</select>
|
|
146
|
-
<Help text={
|
|
147
|
-
<span className="legend">
|
|
148
|
-
{WHEN_VALUES.map((v) => <span key={v} className="lrow"><b>{WHEN_OPTION[v]}</b> ({v}): {TERMS[v]}</span>)}
|
|
149
|
-
</span>
|
|
150
|
-
} />
|
|
173
|
+
{off != null ? <span className="tl red">off: {off}</span> : <span className="dim">runs where it declares</span>}
|
|
151
174
|
<span className="stchip">{SOURCE_CHIP[u.whenSource]}</span>
|
|
152
175
|
<MdHelp text={SOURCE_WORD[u.whenSource]} />
|
|
153
|
-
{
|
|
154
|
-
? <button className="wclear" disabled={s.gateBusy} onClick={() => void s.clearWhen(u.checkId)}>drop the entry</button>
|
|
155
|
-
: null}
|
|
176
|
+
<a href="#" onClick={(e) => { e.preventDefault(); app.showWhen() }}>what runs when</a>
|
|
156
177
|
</span>
|
|
157
|
-
{
|
|
158
|
-
? <span className="cblock">
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
{
|
|
164
|
-
</
|
|
165
|
-
|
|
166
|
-
: null}
|
|
178
|
+
{off != null && u.whenSource === 'off'
|
|
179
|
+
? <span className="cblock"><button disabled={s.gateBusy} onClick={() => void s.turnOn(u.checkId)}>turn it back on</button></span>
|
|
180
|
+
: off == null
|
|
181
|
+
? <span className="cblock">
|
|
182
|
+
<input className="wreason" placeholder="why this repo turns it off (required, it rides in the config)"
|
|
183
|
+
value={reason} onChange={(e) => s.setReason(u.checkId, e.target.value)} />
|
|
184
|
+
<button disabled={s.gateBusy || reason.trim() === ''} onClick={() => void s.setOff(u.checkId)}>turn it off here</button>
|
|
185
|
+
</span>
|
|
186
|
+
: null}
|
|
167
187
|
{g != null && g.state === 'stale'
|
|
168
188
|
? <span className="cblock warnline">the managed block in {g.path} was hand edited: reconcile it, then the switch works again</span>
|
|
169
189
|
: null}
|
|
@@ -321,7 +341,6 @@ export const CheckCard = observer(({ app, id }: { app: RepoSt; id: string }) =>
|
|
|
321
341
|
{whatFacts(u).map((f) => (
|
|
322
342
|
<Row key={f.k} k={f.k} help={f.help}>{f.tone == null ? f.v : <span className={`tl ${f.tone}`}>{f.v}</span>}</Row>
|
|
323
343
|
))}
|
|
324
|
-
<RunWhen app={app} u={u} />
|
|
325
344
|
<Remembered u={u} />
|
|
326
345
|
{/* the help never depends on the second, so it reads the wall
|
|
327
346
|
clock and the row does not repaint on every tick */}
|
|
@@ -338,6 +357,7 @@ export const CheckCard = observer(({ app, id }: { app: RepoSt; id: string }) =>
|
|
|
338
357
|
{u.group == null ? null : <Row k="group" help="a label several checks share, so `--skip <group>` parks all of them at once">{u.group}</Row>}
|
|
339
358
|
<Timings u={u} />
|
|
340
359
|
</div>
|
|
360
|
+
<WhereItRuns app={app} u={u} />
|
|
341
361
|
{u.build && u.checkId === 'test' ? <SlowSuites app={app} /> : null}
|
|
342
362
|
{u.checkId === 'SK014' ? <ProseHits app={app} /> : null}
|
|
343
363
|
<LastOutput app={app} lines={u.lastLines} />
|
|
@@ -49,17 +49,7 @@ export function rememberedFact(u: CachePop): RememberedFact {
|
|
|
49
49
|
const cadence = CADENCE[u.when ?? 'always']
|
|
50
50
|
if (u.never != null)
|
|
51
51
|
return { v: 'never', inputs: [], help: `never cached, so it runs ${cadence}${u.never === '' ? '' : `: ${u.never}`}` }
|
|
52
|
-
return { v: 'never', inputs: [], help: `nothing
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** the four places a check runs, named by what they do. The config keeps its
|
|
56
|
-
* own words (`always`, `release`, `health`, `off`), and these are only what
|
|
57
|
-
* the select under `run when` shows */
|
|
58
|
-
export const WHEN_OPTION: Record<WhenAt, string> = {
|
|
59
|
-
always: 'always',
|
|
60
|
-
release: 'before a release',
|
|
61
|
-
health: 'health pass only',
|
|
62
|
-
off: 'never',
|
|
52
|
+
return { v: 'never', inputs: [], help: `nothing keys it, so it runs ${cadence}. A check file keys it with \`cache: { by: 'files' | 'reads' | 'ttl' | 'fact' }\`` }
|
|
63
53
|
}
|
|
64
54
|
|
|
65
55
|
/** what a check IS: three FACTS, each a short value a reader scans, with the
|