rvlib-shipkit 1.8.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -0
- package/package.json +1 -1
- package/src/assets/gen/comfyts-gen.ts +31 -14
- package/src/assets/gen/generate-roll.ts +20 -7
- package/src/assets/kit-config.ts +9 -0
- package/src/assets/kits/identity-kit.ts +7 -1
- package/src/assets/run-assets.ts +3 -3
- package/src/assets/server/studio-server.ts +3 -3
- package/src/check.ts +5 -2
- package/src/checks/SK001-package-json-shape.check.ts +47 -12
- package/src/checks/SK013-one-way-in-policy.check.ts +8 -7
- package/src/checks/SK017-vscode-hub-task.check.ts +109 -106
- package/src/checks/SK019-tests-run-in-units.check.ts +2 -1
- package/src/checks/SK027-agent-wiki-links.check.ts +5 -2
- package/src/checks/SK029-explicit-check-files.check.ts +25 -8
- package/src/checks/SK034-commit-gates-armed.check.ts +90 -0
- package/src/cli/cmd_Ci.ts +6 -5
- package/src/cli/cmd_Commands.ts +21 -5
- package/src/cli/cmd_Hooks.ts +6 -2
- package/src/cli/cmd_Init.ts +1 -1
- package/src/cli/cmd_Serve.ts +1 -2
- package/src/cli/cmd_Start.ts +1 -1
- package/src/cli.ts +2 -4
- package/src/collections/providers.ts +1 -1
- package/src/core/budget.ts +6 -8
- package/src/core/bun-builtins.ts +36 -0
- package/src/core/bun-tool-cli.ts +23 -0
- package/src/core/canonical-scripts.ts +6 -4
- package/src/core/check-ctx.ts +104 -10
- package/src/core/check-file-names.ts +2 -3
- package/src/core/check-pass.ts +17 -1
- package/src/core/check-runner.ts +32 -10
- package/src/core/config-rewrite.ts +27 -6
- package/src/core/discover-checks.ts +64 -15
- package/src/core/fix-scope.ts +5 -0
- package/src/core/floor-resolve.ts +18 -8
- package/src/core/git-root.ts +22 -12
- package/src/core/job-run.ts +2 -2
- package/src/core/layout.ts +4 -1
- package/src/core/legacy-shapes.ts +40 -0
- package/src/core/one-lib.ts +5 -3
- package/src/core/one-way-in-policy.ts +106 -13
- package/src/core/pre-commit-discovery.ts +106 -0
- package/src/core/retired-checks.ts +2 -0
- package/src/core/run-ci.ts +23 -16
- package/src/core/run-fix.ts +9 -0
- package/src/core/run-fixers.ts +91 -35
- package/src/core/runs-matrix.ts +7 -0
- package/src/core/slots.ts +36 -6
- package/src/core/vendored-read.ts +39 -0
- package/src/core/vendored.ts +82 -0
- package/src/core/waivers.ts +125 -0
- package/src/core/when-block.ts +25 -185
- package/src/core/when.ts +30 -13
- package/src/core/wire.ts +17 -1
- package/src/git-bootstrap.ts +1 -1
- package/src/home/attach.ts +3 -5
- package/src/home/gui-app/ShipkitGui.swift +96 -0
- package/src/home/insights.ts +6 -1
- package/src/home/pm-config.ts +2 -2
- package/src/home/resources.ts +3 -0
- package/src/home/run-daemon.ts +2 -2
- package/src/home/run-unit-action.ts +2 -2
- package/src/hooks/commit-message.ts +2 -14
- package/src/hooks/hook-gates.ts +38 -0
- package/src/hooks/leak-guard.ts +8 -3
- package/src/hooks/one-way-in-guard.ts +8 -2
- package/src/hooks/root-identity.ts +9 -6
- package/src/hooks/run-hooks.ts +72 -41
- package/src/hooks/staged.ts +49 -0
- package/src/hooks/tree-shrink.ts +41 -0
- package/src/init-repo.ts +0 -42
- package/src/ledger/check-table.ts +1 -1
- package/src/ledger/gate-checks.ts +45 -9
- package/src/ledger/ledger.ts +6 -1
- package/src/ledger/repo-view.ts +14 -4
- package/src/ledger/toolchain-identity.ts +4 -2
- package/src/mv/mv-md.ts +6 -1
- package/src/plan/planner.ts +21 -4
- package/src/plan/repo-source.ts +7 -3
- package/src/problems/extract.ts +27 -5
- package/src/problems/problem-line.ts +2 -3
- package/src/release/run-release.ts +30 -16
- package/src/release/unit-gate.ts +12 -5
- package/src/repo-config.ts +51 -27
- package/src/scaffold/files/bunfig.toml +11 -0
- package/src/scaffold/files/vscode-settings.json +1 -2
- package/src/scaffold/jsonc-spans.ts +16 -0
- package/src/scaffold/project-scaffold.ts +43 -77
- package/src/serve/hub-app/check-view.tsx +4 -4
- package/src/serve/hub-app/check-words.ts +1 -0
- package/src/serve/hub-app/checks-svc.ts +1 -2
- package/src/serve/hub-app/home.html +0 -1
- package/src/serve/hub-app/hub-types.ts +0 -2
- package/src/serve/hub-app/index.html +0 -1
- package/src/serve/hub-app/project-list.css +1 -2
- package/src/serve/hub-app/tree-build.ts +3 -2
- package/src/serve/hub-app/tree-svc.ts +2 -2
- package/src/serve/hub-port.ts +3 -3
- package/src/serve/hub-runtime.ts +3 -5
- package/src/serve/project-gutter.ts +5 -2
- package/src/serve/run-lanes.ts +7 -3
- package/src/serve/serve-hub.ts +1 -16
- package/src/serve/tracked-watch.ts +1 -1
- package/src/smoke/run-smoke.ts +13 -6
- package/src/start-repo.ts +15 -3
- package/src/suite/judge-suites.ts +81 -32
- package/src/suite/select.ts +2 -0
- package/src/suite/suite-cli.ts +27 -16
- package/src/suite/suite-cpu-preload.ts +7 -11
- package/src/suite/test-units.ts +7 -4
- package/src/ts/config.ts +12 -5
- package/src/ts/imports.ts +3 -1
- package/src/ts/typecheck-cache.ts +11 -0
- package/src/ts/typecheck-cli.ts +2 -1
- package/src/tui/build-steps-panel.ts +1 -1
- package/src/wiki/scan-md.ts +16 -9
- package/src/checks/SK009-commit-ai-tells.check.ts +0 -131
- package/src/checks/SK014-prose-ste.check.ts +0 -78
- package/src/cli/cmd_Prose.ts +0 -74
- package/src/prose/prose-view.ts +0 -90
- package/src/prose/run-prose.ts +0 -187
- package/src/prose/ste.ts +0 -552
- package/src/scaffold/files/vscode-tasks.json +0 -24
- package/src/serve/hub-app/prose-hits-st.ts +0 -125
- package/src/serve/hub-app/prose-hits-view.tsx +0 -176
- package/src/serve/hub-app/prose-hits.css +0 -21
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// what this commit changes against HEAD, listed ONCE for every git based
|
|
2
|
+
// step of the pre-commit gate: each step spawning its own `git diff --cached`
|
|
3
|
+
// costs a process start per step on every commit. Every call inherits the
|
|
4
|
+
// environment, so a commit made under a private GIT_INDEX_FILE is read from
|
|
5
|
+
// that same index, which is the one git is about to commit.
|
|
6
|
+
import { gitBin } from '../core/git-bin.ts'
|
|
7
|
+
import { gitRoot } from '../core/git-root.ts'
|
|
8
|
+
|
|
9
|
+
/** one staged change: git's status letter (A, M, D, R, C, T, U), the path as
|
|
10
|
+
* `--name-only` prints it, and for a rename or a copy the path it came from */
|
|
11
|
+
export type StagedEntry = { status: string; path: string; from?: string }
|
|
12
|
+
|
|
13
|
+
export type Staged = {
|
|
14
|
+
/** HEAD resolves: false on the first commit of a repo */
|
|
15
|
+
head: boolean
|
|
16
|
+
entries: StagedEntry[]
|
|
17
|
+
/** every staged path, in git's order */
|
|
18
|
+
names: string[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** `git diff --cached --name-status -z`: a status field, then one path, or
|
|
22
|
+
* two for a rename or a copy. NUL separated, so no path is ever quoted */
|
|
23
|
+
export function parseNameStatus(out: string): StagedEntry[] {
|
|
24
|
+
const f = out.split('\0')
|
|
25
|
+
const entries: StagedEntry[] = []
|
|
26
|
+
for (let i = 0; i < f.length; ) {
|
|
27
|
+
const status = f[i++] ?? ''
|
|
28
|
+
if (status === '') break
|
|
29
|
+
const letter = status[0]!
|
|
30
|
+
if (letter === 'R' || letter === 'C') {
|
|
31
|
+
const from = f[i++] ?? ''
|
|
32
|
+
entries.push({ status: letter, path: f[i++] ?? '', from })
|
|
33
|
+
} else entries.push({ status: letter, path: f[i++] ?? '' })
|
|
34
|
+
}
|
|
35
|
+
return entries
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function readStaged(root: string): Staged {
|
|
39
|
+
if (gitRoot(root) == null) return { head: false, entries: [], names: [] }
|
|
40
|
+
// the env is passed on purpose: Bun's default is the environment the
|
|
41
|
+
// process started with, never one a caller set since
|
|
42
|
+
const head = Bun.spawnSync([gitBin(), 'rev-parse', '--verify', '-q', 'HEAD'], { cwd: root, env: process.env, stdout: 'ignore', stderr: 'ignore' }).exitCode === 0
|
|
43
|
+
const p = Bun.spawnSync([gitBin(), 'diff', '--cached', '--name-status', '-z'], { cwd: root, env: process.env, maxBuffer: 64 * 1024 * 1024 })
|
|
44
|
+
// an unreadable index is no answer, and every gate below would read it as
|
|
45
|
+
// "nothing staged" and pass
|
|
46
|
+
if (p.exitCode !== 0) throw new Error(`git diff --cached exited ${p.exitCode}: ${p.stderr.toString().trim().split('\n')[0] ?? ''}`)
|
|
47
|
+
const entries = parseNameStatus(p.stdout.toString())
|
|
48
|
+
return { head, entries, names: entries.map((e) => e.path) }
|
|
49
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// a commit must not drop most of the tree.
|
|
2
|
+
//
|
|
3
|
+
// the failure this exists for: a commit made from a private GIT_INDEX_FILE
|
|
4
|
+
// that was never filled with `git read-tree HEAD` holds only the files staged
|
|
5
|
+
// into it, so git records every other file as deleted and the tree comes out
|
|
6
|
+
// near empty. Every other gate passes, because nothing it reads is wrong.
|
|
7
|
+
import type { Staged } from './staged.ts'
|
|
8
|
+
import { gitBin } from '../core/git-bin.ts'
|
|
9
|
+
|
|
10
|
+
export const ALLOW_MASS_DELETE = 'SHIPKIT_ALLOW_MASS_DELETE'
|
|
11
|
+
|
|
12
|
+
export type ShrinkOutcome =
|
|
13
|
+
| { status: 'skipped'; reason: string }
|
|
14
|
+
| { status: 'clean' }
|
|
15
|
+
| { status: 'shrunk'; headFiles: number; stagedFiles: number; deleted: number }
|
|
16
|
+
|
|
17
|
+
/** the decision, with no git in it. `headFiles` is asked for only when the
|
|
18
|
+
* commit removes more files than it adds, so an ordinary commit pays nothing */
|
|
19
|
+
export function shrinkVerdict(staged: Staged, headFiles: () => number): ShrinkOutcome {
|
|
20
|
+
if (!staged.head) return { status: 'skipped', reason: 'no HEAD yet, nothing to shrink' }
|
|
21
|
+
let deleted = 0
|
|
22
|
+
let added = 0
|
|
23
|
+
for (const e of staged.entries) {
|
|
24
|
+
if (e.status === 'D') deleted++
|
|
25
|
+
else if (e.status === 'A' || e.status === 'C') added++
|
|
26
|
+
}
|
|
27
|
+
if (deleted <= added) return { status: 'clean' }
|
|
28
|
+
const before = headFiles()
|
|
29
|
+
const after = before - deleted + added
|
|
30
|
+
if (after * 2 >= before) return { status: 'clean' }
|
|
31
|
+
if (process.env[ALLOW_MASS_DELETE] === '1') return { status: 'skipped', reason: `${ALLOW_MASS_DELETE}=1` }
|
|
32
|
+
return { status: 'shrunk', headFiles: before, stagedFiles: after, deleted }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function headFileCount(root: string): number {
|
|
36
|
+
const p = Bun.spawnSync([gitBin(), 'ls-tree', '-r', '-z', '--name-only', '--full-tree', 'HEAD'], { cwd: root, env: process.env, maxBuffer: 256 * 1024 * 1024 })
|
|
37
|
+
if (p.exitCode !== 0) throw new Error(`git ls-tree HEAD exited ${p.exitCode}: ${p.stderr.toString().trim().split('\n')[0] ?? ''}`)
|
|
38
|
+
return p.stdout.toString().split('\0').filter(Boolean).length
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const runTreeShrink = (root: string, staged: Staged): ShrinkOutcome => shrinkVerdict(staged, () => headFileCount(root))
|
package/src/init-repo.ts
CHANGED
|
@@ -17,9 +17,6 @@ import { reportToMotherhome } from './core/motherhome.ts'
|
|
|
17
17
|
import { ensureGitRepo, publishRepo, type Run } from './git-bootstrap.ts'
|
|
18
18
|
import { defineRepo, loadRepoConfig, resolveFloor } from './repo-config.ts'
|
|
19
19
|
import { gitignorePolicy } from './checks/SK004-gitignore-policy.check.ts'
|
|
20
|
-
import { vscodeHubTask } from './checks/SK017-vscode-hub-task.check.ts'
|
|
21
|
-
import { editorErgonomics } from './scaffold/project-scaffold.ts'
|
|
22
|
-
import { defaultHubPort } from './serve/hub-port.ts'
|
|
23
20
|
|
|
24
21
|
// This lands as SOURCE in the repo being wired, so its comments answer to that
|
|
25
22
|
// repo's comment style, not to this file's. Dash punctuation is banned there
|
|
@@ -105,7 +102,6 @@ export async function initRepo(root: string, opts: InitOpts = {}): Promise<numbe
|
|
|
105
102
|
// the repos that need ONE WAY IN most are the ones that already exist,
|
|
106
103
|
// so a re-run REPAIRS rather than only seeding a fresh checkout
|
|
107
104
|
await repairOneWayIn(root, cfg, p)
|
|
108
|
-
await furnishEditor(root, cfg, p)
|
|
109
105
|
const mh = reportToMotherhome(root, cfg)
|
|
110
106
|
if (mh.note != null) p(mh.note)
|
|
111
107
|
if (wired && opts.noPublish !== true) await publishRepo(root, { github: opts.github, gh: opts.gh, print: p })
|
|
@@ -139,7 +135,6 @@ export async function initRepo(root: string, opts: InitOpts = {}): Promise<numbe
|
|
|
139
135
|
const g = await gitignorePolicy.repair!(CheckCtx.open({ root, config: defineRepo({ name }) }))
|
|
140
136
|
p(`ok: .gitignore policy block ${g.changed ? 'written' : 'already current'}`)
|
|
141
137
|
await repairOneWayIn(root, defineRepo({ name }), p)
|
|
142
|
-
await furnishEditor(root, { name }, p)
|
|
143
138
|
// what the gate actually runs, in ONE line: a thin floor must be seen at
|
|
144
139
|
// init time rather than discovered after a bad commit slipped through.
|
|
145
140
|
// The per-task resolution is diagnostics, and a `skip:` naming a script
|
|
@@ -170,43 +165,6 @@ export async function initRepo(root: string, opts: InitOpts = {}): Promise<numbe
|
|
|
170
165
|
return 0
|
|
171
166
|
}
|
|
172
167
|
|
|
173
|
-
/** the folder-open story, on the adopt path as well as the start one.
|
|
174
|
-
*
|
|
175
|
-
* `start` scaffolds the whole day-one shape, but only for a dir with no
|
|
176
|
-
* commit history, so every repo that already existed when it was wired got
|
|
177
|
-
* repo.config.ts and the scripts and no .vscode/ at all: opening the folder
|
|
178
|
-
* started nothing, and the hub the config advertises had no way in. These two
|
|
179
|
-
* files are the only half of that scaffold an adopted repo can be handed
|
|
180
|
-
* unasked, because they change no output of any command (project-scaffold.ts
|
|
181
|
-
* owns the line). Create-only, so a .vscode/ the repo brought with it wins. */
|
|
182
|
-
async function furnishEditor(root: string, cfg: { name?: string; serve?: { port?: number } }, p: (line: string) => void): Promise<void> {
|
|
183
|
-
// the least important thing init does, and it runs AFTER repo.config.ts is
|
|
184
|
-
// on disk: an unwritable .vscode (a read-only tree, a path that is a file),
|
|
185
|
-
// or a config with no name at all, must not abort the run before the
|
|
186
|
-
// publish chain, and must not pass in silence either. The port resolution
|
|
187
|
-
// is INSIDE the try for that reason: it reads a config nothing validates
|
|
188
|
-
try {
|
|
189
|
-
const name = repoName(root, undefined, cfg.name)
|
|
190
|
-
const port = cfg.serve?.port ?? defaultHubPort(name)
|
|
191
|
-
if (editorErgonomics(root, { name, port }).length === 0) {
|
|
192
|
-
// the repo's own file stays its own, except the hub task, which
|
|
193
|
-
// rule SK017 keeps feeding the problems pane
|
|
194
|
-
// a file SK017 cannot read is still the repo's own: it is never
|
|
195
|
-
// reported as missing, and it is never rewritten
|
|
196
|
-
let hub: { changed: boolean; summary: string }
|
|
197
|
-
try { hub = await vscodeHubTask.repair!(CheckCtx.open({ root, config: defineRepo({ name }) })) } catch (e) {
|
|
198
|
-
p('ok: .vscode/ is this repo\'s own, left untouched')
|
|
199
|
-
return void p(`WARN .vscode/tasks.json does not parse (${e instanceof Error ? e.message : String(e)}): the hub task gets no problem matcher until it does`)
|
|
200
|
-
}
|
|
201
|
-
return void p(hub.changed ? `ok: ${hub.summary}` : 'ok: .vscode/ is this repo\'s own, left untouched')
|
|
202
|
-
}
|
|
203
|
-
p(`ok: .vscode/tasks.json written — opening the folder starts the hub on ${port}`)
|
|
204
|
-
p(' next: add this stack\'s own watchers (dev server, css) — build/vscode-tasks.md carries the shapes')
|
|
205
|
-
} catch (e) {
|
|
206
|
-
p(`WARN no .vscode/tasks.json here: ${e instanceof Error ? e.message : String(e)} — opening the folder starts nothing, everything else landed`)
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
168
|
/** ONE WAY IN, both halves, through the SAME fixers `shipkit check --fix` runs
|
|
211
169
|
* (build/shipkit/one-way-in.md). Nothing here is a second implementation: rule SK001
|
|
212
170
|
* routes the package.json scripts, rule SK013 writes the harness policy, and
|
|
@@ -87,7 +87,7 @@ export function renderCheckTable(input: CheckTableInput): string {
|
|
|
87
87
|
body.push('')
|
|
88
88
|
body.push(`Everything \`shipkit ci\` and \`shipkit check\` know how to run: ${builds} build step(s) the gate alone runs, ${rows.length - builds} the health pass reports too. ${shipped} ship with the toolkit, ${rows.length - shipped} belong to this repo.`)
|
|
89
89
|
body.push('')
|
|
90
|
-
body.push('- **runs in** — `always` means every `shipkit ci` runs it, so a red one says THIS change is broken. `release` means the quick gate leaves it out and `shipkit ci --full` plus the release path run it. `health` means only the health pass (`shipkit check`) runs it: it reports the world (a box that is off, a cred nobody minted, a page nobody refreshed), it can sit red for months with nothing wrong, and it never stands between you and a commit. `off` means this repo turned it off on purpose, and the reason is in `repo.config.ts
|
|
90
|
+
body.push('- **runs in** — `always` means every `shipkit ci` runs it, so a red one says THIS change is broken. `release` means the quick gate leaves it out and `shipkit ci --full` plus the release path run it. `health` means only the health pass (`shipkit check`) runs it: it reports the world (a box that is off, a cred nobody minted, a page nobody refreshed), it can sit red for months with nothing wrong, and it never stands between you and a commit. `commit` means a git hook runs it on every commit (`shipkit hooks list`). `off` means this repo turned it off on purpose, and the reason is in `repo.config.ts off`.')
|
|
91
91
|
body.push('- **build** — `yes` means the gate alone runs it: it BUILDS (a compiler, a test suite, a bundler), so `shipkit check` leaves it out and reports everything else. A floor slot derives it from the package.json script behind it, and a `*.check.ts` declares it with `build: true`.')
|
|
92
92
|
body.push('- **on failure** — `blocks` stops the run. `warns` prints and carries on. `UNDECLARED` is a defect, not a setting: the check never said and the repo set no default, so the runner treats it as fatal by guess.')
|
|
93
93
|
body.push('- **cache** — `content` is cached while the files it reads are untouched. `world` is cached for its TTL, and a RED that ran is cached too, capped at one hour so the run after a fix sees the fix. `never` runs every time and somebody wrote down why. `UNDECLARED` runs every time and nobody has looked yet, which is the burn-down (`shipkit keys --burn-down`). `SHIPKIT_FRESH=1` busts all of it.')
|
|
@@ -26,6 +26,7 @@ import { CHECK_TIMEOUT_MS, type RunnableCheck, type CheckBody } from '../core/ch
|
|
|
26
26
|
import { FOREVER, ttlMsOf } from '../core/wire.ts'
|
|
27
27
|
import { budgetOf } from '../core/budget.ts'
|
|
28
28
|
import { repoOff, withRepoOff, whenSummary, type WhenDecl } from '../core/when.ts'
|
|
29
|
+
import { HOOK_BUDGET_MS, HOOK_GATES, HOOK_GATES_OWNER, hookGateOff, type HookName } from '../hooks/hook-gates.ts'
|
|
29
30
|
import { cacheText, filesText, NOWHERE, sourced, type CacheDecl, type ResolvedRuns, type Runs } from '../core/runs.ts'
|
|
30
31
|
import { isNamesInput, type CheckDecl } from './git-key.ts'
|
|
31
32
|
import { populationOf } from '../problems/problem-line.ts'
|
|
@@ -131,6 +132,9 @@ export type CheckRow = {
|
|
|
131
132
|
* `repo` = this repo declared it (a package.json script behind a floor
|
|
132
133
|
* slot, a task, a rule, a check file, a check file). */
|
|
133
134
|
from: CheckFrom
|
|
135
|
+
/** a COMMIT GATE: the git hook that runs it (hooks/hook-gates.ts). The
|
|
136
|
+
* runner never does, so every runner context reads false */
|
|
137
|
+
hook?: HookName
|
|
134
138
|
}
|
|
135
139
|
|
|
136
140
|
/** the one axis every check answers: who wrote the declaration */
|
|
@@ -348,15 +352,16 @@ export async function composeChecks(root: string, c: RepoConfig, o: ComposeOpts
|
|
|
348
352
|
// time is not judged and its answer never stands in for a quick one
|
|
349
353
|
out.push({ row, runnable: runnableOf(row, taskBody(t), { unbudgeted: full }) })
|
|
350
354
|
}
|
|
351
|
-
// a slot
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
//
|
|
355
|
+
// a slot the repo turned off never resolves to a task, so without this it
|
|
356
|
+
// had no row at all: no `runs in` value, no reason, and a count of off
|
|
357
|
+
// checks that read zero while a gate step sat dark. It has no command,
|
|
358
|
+
// which is why it declares `never` rather than joining the burn-down of
|
|
359
|
+
// checks nobody has keyed yet.
|
|
356
360
|
for (const id of floor.disabled) {
|
|
357
|
-
const
|
|
361
|
+
const off = repoOff(c, id) ?? { reason: 'turned off', source: 'off' as const }
|
|
362
|
+
const runs = sourced(NOWHERE, 'none', 'repo.config.ts', null, off.reason)
|
|
358
363
|
const row: CheckRow = {
|
|
359
|
-
decl: { id, never: `
|
|
364
|
+
decl: { id, never: `the slot is off, so there is no command to key` },
|
|
360
365
|
title: id,
|
|
361
366
|
description: slotDescription(id, declaredTestUnits(c.units).length > 0),
|
|
362
367
|
gate: false,
|
|
@@ -369,8 +374,8 @@ export async function composeChecks(root: string, c: RepoConfig, o: ComposeOpts
|
|
|
369
374
|
optIn: false,
|
|
370
375
|
fixable: false,
|
|
371
376
|
runs,
|
|
372
|
-
cache: `never:
|
|
373
|
-
when: whenSummary(runs,
|
|
377
|
+
cache: `never: the slot is off`,
|
|
378
|
+
when: whenSummary(runs, off, false),
|
|
374
379
|
deps: null,
|
|
375
380
|
// the repo wrote the line that turned it off
|
|
376
381
|
from: 'repo',
|
|
@@ -518,6 +523,37 @@ export async function composeChecks(root: string, c: RepoConfig, o: ComposeOpts
|
|
|
518
523
|
const body: CheckBody = { cmd: ['bun', d.path], ...(task?.env == null ? {} : { env: task.env }), ...(task?.cwd == null ? {} : { cwd: resolve(root, task.cwd) }) }
|
|
519
524
|
out.push({ row, runnable: runnableOf(row, body, { pointer: `bun ${rel}` }) })
|
|
520
525
|
}
|
|
526
|
+
// THE COMMIT GATES: the git hooks run them (hooks/run-hooks.ts), never the
|
|
527
|
+
// runner, so a row here only makes them enumerable and `off` able to name
|
|
528
|
+
// them. They run in no runner context, and a run by name says where they run
|
|
529
|
+
for (const g of HOOK_GATES) {
|
|
530
|
+
const na = c.nested === true ? 'n/a here: a nested folder, the parent work tree owns the commit gates'
|
|
531
|
+
: g.id === 'blocked-paths' && (c.hooks?.blockedPaths ?? []).length === 0 ? 'n/a here: no hooks.blockedPaths declared' : null
|
|
532
|
+
const off = hookGateOff(c, g.id)
|
|
533
|
+
const hookWords = g.hook === 'both' ? 'the pre-commit and commit-msg hooks' : `the ${g.hook} hook`
|
|
534
|
+
const runs = withRepoOff(sourced(NOWHERE, 'none', 'derived', `a commit gate: ${hookWords} run it on every commit`, na), off)
|
|
535
|
+
const row: CheckRow = {
|
|
536
|
+
decl: { id: g.id, never: `a commit gate runs on the index at commit, never cached` },
|
|
537
|
+
title: g.title,
|
|
538
|
+
description: g.description,
|
|
539
|
+
gate: false,
|
|
540
|
+
build: false,
|
|
541
|
+
blocking: true,
|
|
542
|
+
owner: HOOK_GATES_OWNER,
|
|
543
|
+
group: null,
|
|
544
|
+
timeoutMs: null,
|
|
545
|
+
budgetMs: HOOK_BUDGET_MS,
|
|
546
|
+
optIn: false,
|
|
547
|
+
fixable: false,
|
|
548
|
+
runs,
|
|
549
|
+
cache: `never: ${hookWords}`,
|
|
550
|
+
when: whenSummary(runs, off, false, { hook: true }),
|
|
551
|
+
deps: null,
|
|
552
|
+
from: 'shipkit',
|
|
553
|
+
hook: g.hook,
|
|
554
|
+
}
|
|
555
|
+
out.push({ row, runnable: runnableOf(row, { run: (ctx) => ctx.note(`${g.id} runs in ${hookWords}, never in a gate: \`shipkit hooks list\` shows it`) }) })
|
|
556
|
+
}
|
|
521
557
|
return out
|
|
522
558
|
}
|
|
523
559
|
|
package/src/ledger/ledger.ts
CHANGED
|
@@ -273,9 +273,14 @@ export class Ledger {
|
|
|
273
273
|
/** buffer what a check just measured. Nothing reaches the store until
|
|
274
274
|
* commit() has re-hashed the inputs (rule 2). A READS check with no read
|
|
275
275
|
* set (it reached the world) is never stored (rule 7). */
|
|
276
|
-
record(id: string, outcome: CheckOutcome, reads?: ReadSet | null, notKept?: string | null): void {
|
|
276
|
+
record(id: string, outcome: CheckOutcome, reads?: ReadSet | null, notKept?: string | null, waiverFiles: readonly string[] = []): void {
|
|
277
277
|
const k = this.key(id)
|
|
278
278
|
if (k.key == null) return
|
|
279
|
+
// a READS answer carries every waiving file in its read set. A
|
|
280
|
+
// CONTENT key moves only with its inputs, so a directive outside
|
|
281
|
+
// them could be added or removed under a stored answer
|
|
282
|
+
const outside = k.population === 'content' ? waiverFiles.find((f) => !k.inputOids.some(([p]) => p === '.' || f === p || f.startsWith(`${p}/`))) : undefined
|
|
283
|
+
if (outside != null) { this.ranWhy.set(id, { group: 'not kept', why: `a site in ${outside}, which an in-file directive can waive, sits outside its declared inputs` }); return }
|
|
279
284
|
if (k.population === 'reads') {
|
|
280
285
|
if (reads == null) { this.ranWhy.set(id, { group: 'not kept', why: notKept ?? 'it read outside what the cache can see' }); return }
|
|
281
286
|
this.buffered.set(id, { ...outcome, reads, components: k.components })
|
package/src/ledger/repo-view.ts
CHANGED
|
@@ -7,8 +7,14 @@
|
|
|
7
7
|
// Built once per run and shared: `c.list` in every check and the ledger's
|
|
8
8
|
// replay check read the same snapshot, so a check never lists one tree and is
|
|
9
9
|
// validated against another inside one run.
|
|
10
|
+
//
|
|
11
|
+
// A listing leaves out the repo's vendored dirs (core/vendored.ts), unless
|
|
12
|
+
// its glob names a path inside one: a check never mistakes third party code
|
|
13
|
+
// for the repo's own, and the replay check lists through the same filter.
|
|
10
14
|
import { relative } from 'path'
|
|
11
15
|
import { gitBin } from '../core/git-bin.ts'
|
|
16
|
+
import { listingWithoutVendored } from '../core/vendored.ts'
|
|
17
|
+
import { readVendored } from '../core/vendored-read.ts'
|
|
12
18
|
import { GitScanError, scanPaths, WHOLE_TREE, type ScanResult } from './git-scan.ts'
|
|
13
19
|
|
|
14
20
|
export class RepoView {
|
|
@@ -21,26 +27,30 @@ export class RepoView {
|
|
|
21
27
|
readonly fallback: string | null,
|
|
22
28
|
/** the scan it was read off, handed to the next open as its prior */
|
|
23
29
|
readonly scan: ScanResult | null = null,
|
|
30
|
+
/** the repo's vendored dirs, as its config text declared them at open */
|
|
31
|
+
readonly vendored: readonly string[] = [],
|
|
24
32
|
) {}
|
|
25
33
|
|
|
26
34
|
/** `prior`: an earlier view of the same root in this run. The tree is
|
|
27
35
|
* still staged and hashed fresh, so the list is today's, and an unchanged
|
|
28
36
|
* tree reuses the prior's answer */
|
|
29
37
|
static open(root: string, prior?: RepoView | null): RepoView {
|
|
38
|
+
const vendored = readVendored(root)
|
|
30
39
|
try {
|
|
31
40
|
const scan = scanPaths(root, [WHOLE_TREE], prior?.scan ?? undefined, { treeOnly: true })
|
|
32
41
|
const tree = scan.oidByPath.get(WHOLE_TREE)
|
|
33
42
|
if (tree == null) throw new GitScanError('git could not name the work tree')
|
|
34
|
-
return new RepoView(filesOf(root, tree), null, scan)
|
|
43
|
+
return new RepoView(filesOf(root, tree), null, scan, vendored)
|
|
35
44
|
} catch (e) {
|
|
36
|
-
return new RepoView(walk(root), e instanceof Error ? e.message : String(e))
|
|
45
|
+
return new RepoView(walk(root), e instanceof Error ? e.message : String(e), null, vendored)
|
|
37
46
|
}
|
|
38
47
|
}
|
|
39
48
|
|
|
40
|
-
/** the files matching `glob`, sorted
|
|
49
|
+
/** the files matching `glob`, sorted, the vendored ones left out unless
|
|
50
|
+
* the glob names a path inside one */
|
|
41
51
|
list(glob: string): string[] {
|
|
42
52
|
const g = new Bun.Glob(glob)
|
|
43
|
-
return this.files.filter((f) => g.match(f))
|
|
53
|
+
return listingWithoutVendored(this.files.filter((f) => g.match(f)), glob, this.vendored)
|
|
44
54
|
}
|
|
45
55
|
}
|
|
46
56
|
|
|
@@ -328,8 +328,10 @@ export function toolchainIdentity(repoRoot: string, cliEntry: string): Toolchain
|
|
|
328
328
|
// stands.
|
|
329
329
|
const lib = new LibSource(cliEntry)
|
|
330
330
|
const r = createHash('sha256')
|
|
331
|
-
// the runtime. Free, and a bun upgrade genuinely changes behaviour
|
|
332
|
-
|
|
331
|
+
// the runtime. Free, and a bun upgrade genuinely changes behaviour. The
|
|
332
|
+
// revision too: every canary of one version reports the same version,
|
|
333
|
+
// and a canary upgrade can change what `bun lint` reports
|
|
334
|
+
r.update(`bun=${Bun.version}+${Bun.revision}\n`)
|
|
333
335
|
for (const l of runnerLines(lib, cliEntry)) r.update(`${l}\n`)
|
|
334
336
|
return { id: r.digest('hex').slice(0, 16), install: installOf(root, lib), lib }
|
|
335
337
|
}
|
package/src/mv/mv-md.ts
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
import { execSync } from 'child_process'
|
|
9
9
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'fs'
|
|
10
10
|
import { basename, dirname, join, relative } from 'path'
|
|
11
|
+
import { isVendored } from '../core/vendored.ts'
|
|
12
|
+
import { vendoredAround } from '../core/vendored-read.ts'
|
|
11
13
|
|
|
12
14
|
export type MvIo = { print: (l: string) => void; printErr: (l: string) => void }
|
|
13
15
|
export type MvOpts = { write?: boolean; commit?: boolean }
|
|
@@ -130,8 +132,11 @@ export function movePage(root: string, oldRel: string, newRel: string, io: MvIo,
|
|
|
130
132
|
const bound = (t: string): RegExp =>
|
|
131
133
|
new RegExp('(?<![\\w-])' + t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + (/[/.]$/.test(t) || t.endsWith('.ts') ? '' : '(?![\\w-])'), 'g')
|
|
132
134
|
|
|
135
|
+
// third party code keeps its own references, byte for byte
|
|
136
|
+
const repo = vendoredAround(root)
|
|
137
|
+
const vendored = (repo?.dirs ?? []).map((d) => posix(relative(root, join(repo!.root, d))))
|
|
133
138
|
const tracked = sh('git ls-files', root).split('\n').filter(Boolean)
|
|
134
|
-
.filter((f) => !/\.(png|jpe?g|gif|webp|ico|woff2?|ttf|zip|gz|lockb|lock)$/i.test(f))
|
|
139
|
+
.filter((f) => !/\.(png|jpe?g|gif|webp|ico|woff2?|ttf|zip|gz|lockb|lock)$/i.test(f) && !isVendored(f, vendored))
|
|
135
140
|
|
|
136
141
|
// every path this move relocates, old → new. The sibling dir carries its
|
|
137
142
|
// whole subtree, and the probe is renamed after the page, so its own entry
|
package/src/plan/planner.ts
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
//
|
|
12
12
|
// what keeps it off the cpu: a save burst is asked about once the repo is
|
|
13
13
|
// quiet (QUIET_MS, or MAX_WAIT_MS into a burst that never stops), a check a
|
|
14
|
-
// save moved waits COST_FACTOR times its last measured run
|
|
14
|
+
// save moved waits COST_FACTOR times its last measured run and GATED_HOLD_MS
|
|
15
|
+
// after the latest gate someone else started in its repo, and nothing it
|
|
15
16
|
// starts on its own starts while the load per core is above LOAD_MAX.
|
|
16
17
|
import { availableParallelism, loadavg } from 'os'
|
|
17
18
|
import { CATCH_UP_BEAT_MS, resumeCatchUp, type CatchUpStore } from './catch-up.ts'
|
|
@@ -39,6 +40,9 @@ export type PlanSource = {
|
|
|
39
40
|
checks: () => Promise<PlanCheck[]>
|
|
40
41
|
/** every check's last result on record */
|
|
41
42
|
last: () => Record<string, LastResult>
|
|
43
|
+
/** when the latest run someone asked for started here (a terminal, an
|
|
44
|
+
* agent, a click), never one the plan started; null when none did */
|
|
45
|
+
gatedAt?: () => number | null
|
|
42
46
|
/** the checks a save moved: their answer would not replay */
|
|
43
47
|
changed: () => Promise<string[]>
|
|
44
48
|
/** run these checks now, in the background; resolves to the exit code */
|
|
@@ -88,6 +92,11 @@ export const MAX_WAIT_MS = 10 * 60_000
|
|
|
88
92
|
* measured run: a 30s suite at most every 10 minutes, a 200ms lint at once */
|
|
89
93
|
export const COST_FACTOR = 20
|
|
90
94
|
|
|
95
|
+
/** a save moved check waits this long after the latest run someone else
|
|
96
|
+
* started in its repo (a terminal, an agent, a click): whoever gates that
|
|
97
|
+
* tree is still at work, and its own gate measures what it changed */
|
|
98
|
+
export const GATED_HOLD_MS = 10 * 60_000
|
|
99
|
+
|
|
91
100
|
/** above this 1 minute load average per core, the plan starts nothing on its
|
|
92
101
|
* own: every core already has a process waiting for it */
|
|
93
102
|
export const LOAD_MAX = 1
|
|
@@ -112,6 +121,7 @@ type Repo = {
|
|
|
112
121
|
src: PlanSource
|
|
113
122
|
checks: PlanCheck[]
|
|
114
123
|
last: Record<string, LastResult>
|
|
124
|
+
gatedAt: number | null
|
|
115
125
|
lastRead: number
|
|
116
126
|
marks: Map<string, Mark>
|
|
117
127
|
timer: unknown
|
|
@@ -169,7 +179,7 @@ export class Planner {
|
|
|
169
179
|
async add(src: PlanSource): Promise<void> {
|
|
170
180
|
const old = this.repos.get(src.repo)
|
|
171
181
|
if (old?.timer != null) this.timers.clear(old.timer)
|
|
172
|
-
const r: Repo = { src, checks: [], last: {}, lastRead: -Infinity, marks: old?.marks ?? new Map(), timer: null, burstAt: null, settleAt: null, detecting: null, again: false, error: null }
|
|
182
|
+
const r: Repo = { src, checks: [], last: {}, gatedAt: null, lastRead: -Infinity, marks: old?.marks ?? new Map(), timer: null, burstAt: null, settleAt: null, detecting: null, again: false, error: null }
|
|
173
183
|
this.repos.set(src.repo, r)
|
|
174
184
|
await this.reload(src.repo)
|
|
175
185
|
try {
|
|
@@ -223,6 +233,7 @@ export class Planner {
|
|
|
223
233
|
r.lastRead = now
|
|
224
234
|
try {
|
|
225
235
|
r.last = r.src.last()
|
|
236
|
+
r.gatedAt = r.src.gatedAt?.() ?? null
|
|
226
237
|
} catch (e) {
|
|
227
238
|
this.fail(r.src.repo, e)
|
|
228
239
|
}
|
|
@@ -315,7 +326,9 @@ export class Planner {
|
|
|
315
326
|
// a check pays for its own runs: one save cannot run it again
|
|
316
327
|
// before COST_FACTOR times its last measured run has passed
|
|
317
328
|
const spaced = last?.cost != null ? last.cost.at + COST_FACTOR * last.cost.ms : -Infinity
|
|
318
|
-
|
|
329
|
+
// and it waits for whoever gates this tree by hand to stop
|
|
330
|
+
const gated = r.gatedAt != null ? r.gatedAt + GATED_HOLD_MS : -Infinity
|
|
331
|
+
const at = Math.max(m.changedAt, spaced, gated)
|
|
319
332
|
return { ...base, state: at > now ? 'next' : 'due', reason: 'changed', dueAt: at }
|
|
320
333
|
}
|
|
321
334
|
const lastAt = Math.max(last?.at ?? -Infinity, m.attemptAt ?? -Infinity)
|
|
@@ -354,8 +367,12 @@ export class Planner {
|
|
|
354
367
|
held(): string[] {
|
|
355
368
|
const now = this.now()
|
|
356
369
|
const out: string[] = []
|
|
357
|
-
for (const r of this.repos.values())
|
|
370
|
+
for (const r of this.repos.values()) {
|
|
358
371
|
if (r.settleAt != null) out.push(`settling: ${r.src.repo}, ${Math.max(0, Math.ceil((r.settleAt - now) / 1_000))}s`)
|
|
372
|
+
const until = r.gatedAt == null ? null : r.gatedAt + GATED_HOLD_MS
|
|
373
|
+
if (until != null && until > now && [...r.marks.values()].some((m) => m.changedAt != null))
|
|
374
|
+
out.push(`deferred: ${r.src.repo}, a gate someone started there ${Math.floor((now - r.gatedAt!) / 60_000)}m ago, so what a save moved waits ${Math.ceil((until - now) / 60_000)}m`)
|
|
375
|
+
}
|
|
359
376
|
if (this.holdReason != null) out.push(this.holdReason)
|
|
360
377
|
return out
|
|
361
378
|
}
|
package/src/plan/repo-source.ts
CHANGED
|
@@ -82,12 +82,15 @@ export function readLastCosts(path: string): Record<string, { at: number; ms: nu
|
|
|
82
82
|
return out
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
/** when the latest run on record started,
|
|
86
|
-
|
|
85
|
+
/** when the latest run on record started, or null. `asked`: only a run
|
|
86
|
+
* someone asked for (a terminal, an agent, a click), never one the plan
|
|
87
|
+
* started on its own */
|
|
88
|
+
export function lastStarted(path: string, o: { asked?: boolean } = {}): number | null {
|
|
87
89
|
const opened = openDb(path, { create: false })
|
|
88
90
|
if (opened == null) return null
|
|
89
91
|
try {
|
|
90
|
-
const
|
|
92
|
+
const sql = o.asked === true ? "select max(started) as at from invocations where by != 'schedule'" : 'select max(started) as at from invocations'
|
|
93
|
+
const r = opened.db.query(sql).get() as { at: number | null } | null
|
|
91
94
|
return r?.at == null ? null : Number(r.at)
|
|
92
95
|
} catch {
|
|
93
96
|
return null
|
|
@@ -144,6 +147,7 @@ export function repoSource(d: RepoSourceDeps): PlanSource {
|
|
|
144
147
|
for (const [id, cost] of Object.entries(costs)) if (all[id] != null) all[id] = { ...all[id]!, cost }
|
|
145
148
|
return all
|
|
146
149
|
},
|
|
150
|
+
gatedAt: () => (d.record === '' ? null : lastStarted(d.record, { asked: true })),
|
|
147
151
|
changed: async () => {
|
|
148
152
|
const decls = rows.map((r) => r.decl)
|
|
149
153
|
if (decls.length === 0) return []
|
package/src/problems/extract.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// file:line findings out of what a check printed, one function per format the
|
|
2
|
-
// floor meets: tsc,
|
|
3
|
-
//
|
|
2
|
+
// floor meets: tsc, bun lint (the unix format the built-in lint slot asks
|
|
3
|
+
// for), oxlint (both its piped and its graphical output, kept while a repo
|
|
4
|
+
// still lints with it: SK001 warns on one, and it dies with the last), eslint's stylish
|
|
5
|
+
// output, a failing bun test, and a shipkit wire line naming a file.
|
|
4
6
|
// Every one feeds the one Problem shape (problem-line.ts).
|
|
5
7
|
//
|
|
6
8
|
// a finding the source cannot anchor in a file is dropped rather than pinned
|
|
@@ -62,6 +64,20 @@ export function fromTsc(text: string, ctx: ExtractCtx): Problem[] {
|
|
|
62
64
|
return out
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
const BUN_LINT_UNIX = /^(.+?):(\d+):(\d+): (.*) \[(Error|Warning)\/([^\]]+)\]$/
|
|
68
|
+
|
|
69
|
+
/** bun lint `--format unix`: `src/a.ts:1:7: msg [Warning/no-unused-vars]` */
|
|
70
|
+
export function fromBunLint(text: string, ctx: ExtractCtx): Problem[] {
|
|
71
|
+
const out: Problem[] = []
|
|
72
|
+
for (const raw of stripAnsi(text).split('\n')) {
|
|
73
|
+
const m = BUN_LINT_UNIX.exec(raw)
|
|
74
|
+
if (m == null) continue
|
|
75
|
+
const file = repoFile(ctx, m[1]!, false)
|
|
76
|
+
if (file != null) out.push({ file, line: Number(m[2]), col: Number(m[3]), severity: m[5] === 'Error' ? 'error' : 'warning', check: ctx.check, message: `${m[6]} ${m[4]!.trim()}` })
|
|
77
|
+
}
|
|
78
|
+
return out
|
|
79
|
+
}
|
|
80
|
+
|
|
65
81
|
const OXLINT_PIPED = /^(.+?):(\d+):(\d+): (error|warning) (\S+?): (.*)$/
|
|
66
82
|
const OXLINT_HEAD = /^\s*(x|!|×|⚠)\s+(.+)$/
|
|
67
83
|
const OXLINT_AT = /^\s*(?:,-|╭─)\[(.+):(\d+):(\d+)\]\s*$/
|
|
@@ -148,14 +164,20 @@ export function fromBunTest(text: string, ctx: ExtractCtx): Problem[] {
|
|
|
148
164
|
|
|
149
165
|
const WIRE_REF = /((?:[\w@.~-]+\/)*[\w@.~-]+\.[A-Za-z0-9]+):(\d+)(?::(\d+))?/g
|
|
150
166
|
|
|
151
|
-
/** a FAIL or WARN wire line
|
|
152
|
-
*
|
|
167
|
+
/** a FAIL or WARN wire line: at the site the check reported when it gave one,
|
|
168
|
+
* else at every `path:line` or `path:line:col` the text names. The path must
|
|
169
|
+
* be a file this repo has: a url and a `host:port` look the same */
|
|
153
170
|
export function fromWire(lines: Line[], ctx: ExtractCtx): Problem[] {
|
|
154
171
|
const out: Problem[] = []
|
|
155
172
|
for (const l of lines) {
|
|
156
173
|
if (l.kind !== 'fail' && l.kind !== 'warn') continue
|
|
157
174
|
const severity: Severity = l.kind === 'fail' ? 'error' : 'warning'
|
|
158
175
|
const message = l.text.replace(/^\[[^\]]+\]\s*/, '').trim()
|
|
176
|
+
if (l.at != null) {
|
|
177
|
+
const file = repoFile(ctx, l.at.file, true)
|
|
178
|
+
if (file != null) out.push({ file, line: l.at.line ?? 1, col: l.at.col ?? 1, severity, check: ctx.check, message })
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
159
181
|
for (const m of l.text.matchAll(WIRE_REF)) {
|
|
160
182
|
const before = l.text.slice(0, m.index)
|
|
161
183
|
if (/[a-z]+:\/\/\S*$/i.test(before)) continue
|
|
@@ -173,7 +195,7 @@ export function extractProblems(src: { lines: Line[]; output?: string }, ctx: Ex
|
|
|
173
195
|
const text = src.output ?? ''
|
|
174
196
|
const all = [
|
|
175
197
|
...fromWire(src.lines, ctx),
|
|
176
|
-
...(text === '' ? [] : [...fromTsc(text, ctx), ...fromOxlint(text, ctx), ...fromEslint(text, ctx), ...fromBunTest(text, ctx)]),
|
|
198
|
+
...(text === '' ? [] : [...fromTsc(text, ctx), ...fromBunLint(text, ctx), ...fromOxlint(text, ctx), ...fromEslint(text, ctx), ...fromBunTest(text, ctx)]),
|
|
177
199
|
]
|
|
178
200
|
const seen = new Set<string>()
|
|
179
201
|
return all.filter((p) => {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// the ONE shape a problem takes on the hub terminal, and the vscode problem
|
|
2
|
-
// matcher that reads it back
|
|
3
|
-
//
|
|
4
|
-
// never drift apart.
|
|
2
|
+
// matcher that reads it back, in one file so the two can never drift apart.
|
|
3
|
+
// No house task carries the matcher any more (SK017 removes hub tasks).
|
|
5
4
|
//
|
|
6
5
|
// a run prints one FRAME: the begin line, one problem line per finding, the
|
|
7
6
|
// end line. The matcher is a background one, so vscode clears the pane at the
|