rvlib-shipkit 1.9.0 → 2.0.1
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 +38 -0
- package/package.json +1 -1
- 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 +3 -3
- 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/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 +19 -6
- package/src/core/config-rewrite.ts +27 -6
- package/src/core/discover-checks.ts +30 -10
- package/src/core/floor-resolve.ts +16 -7
- 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 +1 -1
- 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 +16 -10
- 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 +1 -1
- 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/fleet/update-fleet.ts +11 -2
- package/src/git-bootstrap.ts +1 -1
- package/src/home/attach.ts +3 -5
- package/src/home/insights.ts +6 -1
- package/src/home/pm-config.ts +2 -2
- 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/problems/extract.ts +27 -5
- package/src/problems/problem-line.ts +2 -3
- package/src/release/run-release.ts +13 -8
- package/src/repo-config.ts +44 -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 +0 -3
- package/src/serve/serve-hub.ts +1 -16
- package/src/serve/tracked-watch.ts +1 -1
- package/src/start-repo.ts +15 -3
- package/src/suite/judge-suites.ts +29 -28
- package/src/suite/select.ts +2 -0
- package/src/suite/suite-cli.ts +6 -3
- 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/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
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,38 @@
|
|
|
1
|
+
## 2.0.1
|
|
2
|
+
|
|
3
|
+
- **`sk fleet --update --major` crosses the major again.** Each repo's guarded run now stays in its own process (`SHIPKIT_NO_HOME`), because a check the home answered never saw `SHIPKIT_ALLOW_MAJOR` and refused the breaking jump, naming the very command that was running.
|
|
4
|
+
|
|
5
|
+
## 2.0.0
|
|
6
|
+
|
|
7
|
+
- **A clean commit prints one line**, `ok: N commit gate(s) clean`. A gate that fails still prints whole, and a WARN or a waived hit inside a passing gate still prints.
|
|
8
|
+
- **A green `sk ci` prints one line, the verdict.** Each check's `ok:` and `skip:` line and the `ran:` reasons go behind `--verbose`, and STATUS.md still holds them. A WARN, a FAIL with its fix, what the fix pass rewrote and a wait for a machine slot always print. A gate of 65 checks went from 86 lines to 2.
|
|
9
|
+
- **A check's default time budget is 1 s in process and 5 s for a build step, a `cmd` check or a world probe** (it was 2 s and 10 s). A check that passes over it still only warns. The per suite test budget is unchanged.
|
|
10
|
+
- **The suite runner never discovers a test file under a dot dir, as bun does not.** So a repro archived under `.shipkit/repros/` costs no run, `tests.select` included. `.shipkit/repros/` is a tracked folder of the SK004 block.
|
|
11
|
+
- **`sk commands --compact`** renders one line per command, for a page every session loads.
|
|
12
|
+
- **No VS Code task starts the shipkit hub any more.** The hub runs in the home, so `sk start` and `sk init` write no `.vscode/tasks.json`, and the scaffold template is gone. SK017 now runs in the gate (a WARN, never a block) and its repair is in the fix pass of every `sk ci`: it removes a hub task (`bun run hub`, or a `🚀 shipkit` label) and the hub's `dependsOn` entries, a wrapper left starting nothing, and the old scaffold comment about the hub. A wrapper left starting ONE task folds into it, which then opens with the folder. A file left with no task is deleted. The `👁 TSC` rule is unchanged, and a package published to npm is still never rewritten. Run by hand: `sk check --fix SK017`.
|
|
13
|
+
- **`vendored: ['<dir>', …]` in repo.config.ts keeps third party code out of every tool that treats the tree as the repo's own source.** A vendored library (a git subtree, a copied package) stays byte identical to upstream. The import fixer and imports:check never crawl it and never adopt its package.json as one of the repo's packages, which rewrote every relative import of a vendored workspace into a package name form. Cycles and the source graph skip it, the on save `imports <file>` leaves a vendored file alone, `bun lint` gets `--ignore-pattern=<dir>/**`, `c.list` leaves it out of every check's listing (unless the glob names a path inside it), check discovery and the md scan never enter it, and `shipkit mv` never rewrites a reference inside it. A fixer the toolkit cannot scope (a package.json `imports:fix` script) still runs, and whatever it wrote under a vendored dir is put back byte for byte, an uncommitted edit of the repo's own included, with a WARN naming it. Cache keys, watchers and the leak guard still read vendored files. The value is a literal array of strings, because the tools that never load the config read it as text, and the config load refuses a value the text cannot show. A repo running an older copy (its hooks, its own scripts) refuses the key as unknown until it upgrades.
|
|
14
|
+
- **A directive id ends on a letter or digit.** `sk-disable-line RV027: why` reads as the id `RV027` with no reason, so it warns, where it used to name an id `RV027:` that matched nothing.
|
|
15
|
+
- **A failed linter or tsc names its errors on the gate.** A build step that prints no wire FAIL line now shows its parsed errors under the headline, up to 20, as `file:line:col: rule message`, where it used to show only its closing recap and a log path.
|
|
16
|
+
- **A finding carries its site, and the file it sits in can waive it.** `c.fail(msg, { fix, at: { file, line, col } })` and `c.warn` take the site beside the fix pointer, and a plain string is still the pointer, so no check file changes. A check that passes `{ fix, at }` needs this release: an older copy prints the object as its pointer. The site rides on the wire line through the cache replay into the problems pane, which no longer has to find `path:line` in the text, and a message that does not name its file gets the site in front. A directive in the file's own comment waives a site, and only with a reason: `sk-disable-next-line <ID>[, <ID>…] -- <reason>`, `sk-disable-line`, `sk-disable-file`. A waived site is neither a FAIL nor a WARN: the check prints `skip: [<ID>] 2 sites waived in their files`, and `--verbose` and STATUS.md list each one. A directive with no reason waives nothing and WARNs, and so does one that waives nothing in a file the check read. The waiving file is read through `c`, so it joins the answer's key, and a check keyed on `files` stores no answer that a directive outside its inputs decided. An in process `run` body applies them, a `cmd` check does not.
|
|
17
|
+
- **SK027 reports where it found each problem.** A dangling ref carries the line it sits on and an orphan its file, so the problems pane opens the page and an orphan kept on purpose can waive itself with `sk-disable-file SK027 -- <reason>`.
|
|
18
|
+
- **`sk start` on a repo with history seeds the house files when it writes the house include lines.** The `agent/` wiki the second include routes at, the `.shipkit/reflections/` seed and the two `.claude/` policy files land create-only (`adoptHouse`), so every file the repo already has wins and nothing is committed over its history. A start with no house still writes none of them.
|
|
19
|
+
- **`off: { <id>: '<why>' }` is the one switch, for a check, a floor slot and a commit gate alike.** The commit gates take ids: `tree-shrink`, `leak-guard`, `blocked-paths`, `root-identity`, `commit-message` and `commit-language`. `commit-message` off takes `commit-language` with it. An `off` id the gate cannot find still fails, a misspelled gate id included.
|
|
20
|
+
- **The commit gates are rows of the inventory.** `sk checks`, `sk when` and the hub list each one with its hook in the commit column (`pre-commit`, `commit-msg` or `both`), its owner page and its `off` reason. They still run only from the git hooks, which load nothing new, and `sk ci --only <gate>` says where the gate runs instead of running it.
|
|
21
|
+
- **`hooks.disable` and `defaults: { <slot>: false }` are retired, and still load.** Each reads as off with the reason `was hooks.disable: ['<id>'], no reason recorded` or `was defaults: { <slot>: false }, no reason recorded`, so every surface shows that nobody wrote one. SK029 fails while a config carries them, and the fix pass of `sk ci` ports them into `off`: the comment written right above the entry becomes its reason, and a shape it cannot read (a multi line `disable` array) is named for a person and left loading. `sk hooks list` prints an off gate as `(OFF: <reason>)` where it printed `(DISABLED by config)`.
|
|
22
|
+
- **A floor slot turned off through `off` is off everywhere, as `defaults: { <slot>: false }` was.** It resolves to no task, SK001 no longer routes or asks for its script, SK013 no longer denies its raw tool, SK019 no longer reads its script, and its fixer slot stays off. Before, `off` stopped the slot from running but kept routing and denying it. A hub switch that turns a slot off now lifts its deny rule too, on the next fix pass.
|
|
23
|
+
- **SK034 commit gates armed.** It FAILs a repo where git would not run the leak guard and the message gate: an unset `core.hooksPath`, or a foreign `pre-commit` or `commit-msg` that never calls shipkit, with the guarded line to chain. This was half of SK009, which only checked `commit-msg`, and it now covers the hook that carries the leak guard too.
|
|
24
|
+
- **`sk ci --only <check> --fix` repairs only what the named checks own.** Their own ci repair, and the fixer slot that governs one of them (`imports:fix` for `imports:check`). Before, `--fix` ran every fixer slot over the whole tree.
|
|
25
|
+
- **The STE prose rule is gone: SK014, `sk prose`, the hub's prose page and the `prose` config key.** No rule polices prose style any more. A `repo.config.ts` that still declares `prose` fails at load with "prose was removed with rule SK014: delete the key", and an `off` or `ruleExempt` entry that names SK014 or SK009 fails the same way, naming the retired rule.
|
|
26
|
+
- **The AI attribution guard is gone: SK009 and the attribution half of the commit-msg hook.** The hook keeps the dash and French checks, and `hooks.disable: ['commit-message']` now lifts the whole message gate. The scaffold still writes the `.claude/settings.json` attribution silencer, which is harness config and reaches no agent's context.
|
|
27
|
+
- **`bun lint` is the house linter.** With no `lint` script and a `[lint]` section in `bunfig.toml`, the lint slot runs `bun lint --format unix`, and the problems pane reads its findings. It needs bun 1.4.4 (canary today): an older bun gets a failing line that names `bun upgrade --canary`, never a `lint` script that calls itself.
|
|
28
|
+
- **Nothing formats in the gate.** The fix pass has no formatter slot any more: a `fmt` or `format` script, and a `[fmt]` section in `bunfig.toml`, are never run by `sk ci`, locally or under CI, so verify mode no longer fails a commit on formatting. `bun fmt` stays a manual tool, styled by `[fmt]`. SK001 no longer asks for `[fmt]` or a `fmt` script, and the `pretty` alias WARN is gone with it.
|
|
29
|
+
- **oxlint and oxfmt are retired.** SK001 WARNs, one line per tool, on a dependency (`oxlint-tsgolint` included) or a script body that names either, with the migration: port `.oxlintrc.json` into `[lint]` for `bun lint`, then delete the dependency, each script that is only the tool, and the tool's config file. A script that does more (a code generator that formats its output) is told to lose the step, never deleted. It never repairs this itself: a linter swap changes which rules gate the repo, and dropping a dependency moves `bun.lock`, which only an install rewrites.
|
|
30
|
+
- **A leftover `lint` script can no longer take the built-in's place.** bun runs a package.json script of that name instead of `bun lint`, flags and all, so the built-in now runs through `src/core/bun-tool-cli.ts`, which sets `npm_lifecycle_event` and drops an inherited `npm_package_json`, the two signals bun reads, and SK001 WARNs on a `lint` or `fmt` script that shadows a declared section.
|
|
31
|
+
- **A canary upgrade invalidates cached answers.** The toolchain identity hashes `Bun.revision` beside `Bun.version`, which every canary of one version shares.
|
|
32
|
+
- **`lint` is no longer a routed script.** SK001 stops asking for `lint` to route through `sk ci --only lint`, its fixer stops writing `lint:raw`, and the one-way-in deny list drops `oxlint` and `eslint`. A `[lint]` section meets SK001's baseline as a script did. A repo that already routes `lint` keeps working.
|
|
33
|
+
- **The scaffold writes `bunfig.toml` with `[fmt]` and `[lint]`, and no `lint` or `fmt` script.** The toolchain drops `oxlint` and `oxfmt`, and the editor settings drop `oxc.enable`.
|
|
34
|
+
- **A test suite is judged on its own CPU in the parallel phase, never on the files that ran before it in its worker.** A suite over `tests.budgetMs` in wall time skipped its lone run only when its own CPU plus everything its worker burned before it stayed under the budget, and after a worker's first file that sum is always over. So on a loaded machine nearly every suspect ran alone, one after the other: in a large repo 27 lone runs took 53 s of a 97.6 s test check. A suspect whose own CPU in the parallel phase, children included, is inside the same allowance a lone run gets (`cpuAllowance`) is now cleared there. A module that several suites share is charged to the first suite of each worker that loads it. A suite whose own figure is past its allowance, whose wall can hide a wait, or that ends with a child still alive runs alone as before, and only a lone run can fail the task. Measured on that repo, 760 suites under eight CPU burners: 19 lone runs and 47.1 s at load 13.5, then 0 lone runs and 27.0 s at load 24.5. The preload's line is now `<file>\t<cpu ms>`.
|
|
35
|
+
|
|
1
36
|
## 1.9.0
|
|
2
37
|
|
|
3
38
|
- **A commit in a linked worktree passes the pre-commit hook.** git sets GIT_DIR for a hook there, and the installed copy then asked git where the house sits and got the worktree back, so it never handed off to the house source. Then it ran itself and failed every built-in check with ENOENT on a `?v=` stamped path. A directory with its own `.git` is now asked with the locating variables stripped, and an installed copy that runs itself (CI, `SHIPKIT_NO_HOME`) loads its own check files.
|
|
@@ -17,6 +52,9 @@
|
|
|
17
52
|
- **The gate typecheck keeps its own incremental cache, and a release can publish with bun.** The built-in typecheck passes `--incremental --tsBuildInfoFile <project dir>/node_modules/.cache/shipkit/<config>.tsbuildinfo` for every project, so a `tsc --watch` task or a raw script on the same tsconfig no longer writes the cache the gate reads. A config without `incremental` gets it too: rvlib-shipkit's own typecheck went from 0.81 s to 0.35 s warm. `release.publisher: 'bun'` makes `sk release` and `sk bump` run `bun publish --access public`, with the token from `release.npmTokenCommand` in `NPM_CONFIG_TOKEN`, for a repo that tests its tarball with `bun pm pack`. The default stays npm, and an unknown publisher fails `defineRepo`.
|
|
18
53
|
|
|
19
54
|
- **`alert`, `confirm` and `prompt` work in the shipkit app and in every process app.** The app window answered each one at once with no panel: `confirm` returned false and `prompt` null, so a page button behind one did nothing. Each now shows as a sheet on its window. The prompt's text field holds the default text, selected. Return is OK and Esc is Cancel. An app picks it up the next time `sk gui` or `sk gp open <id> --app` opens it, because a changed source rebuilds the app and quits the running copy.
|
|
55
|
+
- **The pre-commit hook refuses a commit that drops most of the tree.** A commit that keeps under half of HEAD's files fails `tree-shrink`, which is what a commit from a private `GIT_INDEX_FILE` never filled with `git read-tree HEAD` does. `SHIPKIT_ALLOW_MASS_DELETE=1` lifts it for one deliberate mass delete, and `hooks.disable: ['tree-shrink']` for good.
|
|
56
|
+
- **The pre-commit hook imports only the check files that run at commit.** A table under the git dir remembers, per file stamp, whether a check file declares `onPreCommit`, so a check file that never runs at commit costs the commit nothing while it stands still. A repo check file whose import takes over 30ms is named with a WARN on the commit that pays for it. The git steps share one staged listing, and the commit checks share one pass that records no config reads. Warm, on a consumer shaped fixture: 189ms before, 110ms after.
|
|
57
|
+
- **SK013 also writes `Bash(git stash*)` into every repo's deny list, and the guard refuses a stash in the forms the deny row cannot see**: after git's own options (`git -C dir stash`), inside `bash -c` or `eval`. `git stash list` and `git stash show` pass the guard.
|
|
20
58
|
|
|
21
59
|
- **The `prompt` and `seed` vars contract binds only the workflows a kit kind drives.** Loading the workflows folder no longer refuses a module that lacks them, so a workflow the repo drives with its own tools (portraits, voice, music) names its vars freely. The check runs when the atelier resolves the workflow for a kind, and the error names that kind.
|
|
22
60
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rvlib-shipkit",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"funding": "https://github.com/sponsors/rvion",
|
|
5
5
|
"description": "All-in-one repo companion: ci gate (task DAG), auto-discovered health checks, release-contract fulfillness, asks queue, md-wiki tools, asset atelier, live tui",
|
|
6
6
|
"license": "MIT",
|
package/src/check.ts
CHANGED
|
@@ -45,7 +45,7 @@ export type CheckDef = {
|
|
|
45
45
|
* Default: the id. */
|
|
46
46
|
title?: string
|
|
47
47
|
/** what MUST BE TRUE, in the present tense: ONE line, at most 90
|
|
48
|
-
* characters, as in `package.json routes
|
|
48
|
+
* characters, as in `package.json routes typecheck and test
|
|
49
49
|
* through the gate`. Never what red means, red is its negation. It
|
|
50
50
|
* renders at the top of the check page and as the tree row's tooltip.
|
|
51
51
|
* A title names the check, this states the invariant it holds. */
|
|
@@ -119,7 +119,7 @@ export type CheckDef = {
|
|
|
119
119
|
/** what a healthy run takes, in ms, distinct from the ceiling: a run that
|
|
120
120
|
* passes over it prints a WARN and reads yellow, and never blocks.
|
|
121
121
|
* Judged on the wall time of a run that measured, at the quick tier.
|
|
122
|
-
* Default
|
|
122
|
+
* Default 1000 for an in process body, 5000 for a build step, a `cmd`
|
|
123
123
|
* or a world probe (cache `ttl`, `fact` or `never`, core/budget.ts).
|
|
124
124
|
* The repo's `budgets` wins over it. */
|
|
125
125
|
budgetMs?: number
|
|
@@ -329,6 +329,7 @@ export async function runCheckBody(
|
|
|
329
329
|
c.lines = tagCheckLines(u, c.lines)
|
|
330
330
|
return
|
|
331
331
|
}
|
|
332
|
+
c.checkId = u.id
|
|
332
333
|
try {
|
|
333
334
|
await u.run?.(c)
|
|
334
335
|
} catch (e) {
|
|
@@ -340,6 +341,8 @@ export async function runCheckBody(
|
|
|
340
341
|
// and it must not enter the body's read set
|
|
341
342
|
if ((c.failed || c.warned) && !c.lines.some((l) => l.kind === 'fix-fail' || l.kind === 'fix-warn'))
|
|
342
343
|
c.lines.push({ kind: c.failed ? 'fix-fail' : 'fix-warn', text: c.ownerPointer(u.owner) })
|
|
344
|
+
// after the pointer, so it binds to the body's own lines
|
|
345
|
+
c.settleWaivers()
|
|
343
346
|
c.lines = tagCheckLines(u, c.lines)
|
|
344
347
|
}
|
|
345
348
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// whatever the floor resolution read.
|
|
7
7
|
//
|
|
8
8
|
// It also enforces the package.json half of ONE WAY IN (build/shipkit/one-way-in.md):
|
|
9
|
-
// `test
|
|
9
|
+
// `test` and `typecheck` ROUTE to `shipkit ci --only <id>` and the
|
|
10
10
|
// tool body they used to hold sits at `<id>:raw`. That is what makes a cache
|
|
11
11
|
// worth having: `bun run test` is the reflex, and the reflex has to land in
|
|
12
12
|
// shipkit rather than beside it.
|
|
@@ -14,10 +14,13 @@ import { canonicalBodies, conditionalBodies, inferBin, ROUTED_SCRIPTS, rawName,
|
|
|
14
14
|
import type { CheckCtx } from '../core/check-ctx.ts'
|
|
15
15
|
import { checkFloorReader, resolveFloor } from '../core/floor-resolve.ts'
|
|
16
16
|
import { rootPublishesNpm } from '../core/repo-facts.ts'
|
|
17
|
+
import { bunfigDeclares } from '../core/bun-builtins.ts'
|
|
18
|
+
import { repoOff } from '../core/when.ts'
|
|
17
19
|
import type { RepoConfig } from '../repo-config.ts'
|
|
18
20
|
import { check, type FixResult } from '../check.ts'
|
|
19
21
|
|
|
20
|
-
type
|
|
22
|
+
type Deps = Record<string, string>
|
|
23
|
+
type Pkg = { name?: string; private?: boolean; license?: string; scripts?: Record<string, string>; dependencies?: Deps; devDependencies?: Deps; optionalDependencies?: Deps }
|
|
21
24
|
|
|
22
25
|
const OWNER = 'meta/rv-conventions/package-json.md'
|
|
23
26
|
|
|
@@ -26,11 +29,18 @@ const OWNER = 'meta/rv-conventions/package-json.md'
|
|
|
26
29
|
// folder must not be told to rename)
|
|
27
30
|
const ALIASES: Record<string, { canonical: string; bodyMatch: RegExp }> = {
|
|
28
31
|
'serve': { canonical: 'hub', bodyMatch: /(?:^|\s)(?:shipkit|\S*shipkit\S*\/cli\.ts) serve\b/ },
|
|
29
|
-
'pretty': { canonical: 'fmt', bodyMatch: /./ },
|
|
30
32
|
'check-types': { canonical: 'typecheck', bodyMatch: /./ },
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
const BASELINE = ['test', 'lint'
|
|
35
|
+
const BASELINE = ['test', 'lint'] as const
|
|
36
|
+
|
|
37
|
+
// tools bun's built-ins replaced. Never repaired here: a linter swap changes
|
|
38
|
+
// which rules gate the repo, and dropping a dependency moves bun.lock, which
|
|
39
|
+
// only an install rewrites
|
|
40
|
+
const RETIRED: Array<{ tool: string; deps: string[]; config: string; how: string }> = [
|
|
41
|
+
{ tool: 'oxlint', deps: ['oxlint', 'oxlint-tsgolint'], config: '.oxlintrc.json', how: 'port its rules into [lint] in bunfig.toml (`bun lint` is the house linter), then' },
|
|
42
|
+
{ tool: 'oxfmt', deps: ['oxfmt'], config: '.oxfmtrc.json', how: 'nothing formats in the gate (`bun fmt` styled by [fmt] is manual), so' },
|
|
43
|
+
]
|
|
34
44
|
|
|
35
45
|
// no per-line fix pointers here — the rule framework appends ONE pointer
|
|
36
46
|
// (docsBase + OWNER) to a failing/warning run, the same one-per-severity
|
|
@@ -94,7 +104,7 @@ export function evaluatePackageJson(t: CheckCtx): void {
|
|
|
94
104
|
// route to, so it is not governed.
|
|
95
105
|
const floor = resolveFloor(c, t.root, checkFloorReader(t))
|
|
96
106
|
for (const id of ROUTED_SCRIPTS) {
|
|
97
|
-
if (routedExempt(c).has(id) || c
|
|
107
|
+
if (routedExempt(c).has(id) || repoOff(c, id) != null) continue
|
|
98
108
|
const body = scripts[id]
|
|
99
109
|
if (body == null) continue
|
|
100
110
|
if (!routesToShipkit(body)) {
|
|
@@ -108,13 +118,38 @@ export function evaluatePackageJson(t: CheckCtx): void {
|
|
|
108
118
|
// `test` is asked for only once the repo HAS tests: the fixer deliberately
|
|
109
119
|
// refuses to scaffold it before that (a fixer never manufactures a
|
|
110
120
|
// failure), so nagging for it would be a WARN no machine may clear. For
|
|
111
|
-
// the same reason `lint`
|
|
121
|
+
// the same reason `lint` waits for a source file to read. It is met by
|
|
122
|
+
// bun's built-in once bunfig.toml declares [lint]
|
|
112
123
|
const tests = hasTestFiles(testFiles(t))
|
|
113
124
|
const source = tests || hasSourceFiles(t)
|
|
114
|
-
const
|
|
115
|
-
|
|
125
|
+
const bunfig = t.read('bunfig.toml')
|
|
126
|
+
// a lint slot turned off has no built-in to declare
|
|
127
|
+
const baseline = BASELINE.filter((n) => (n === 'test' ? tests : source) && !(n === 'lint' && repoOff(c, 'lint') != null))
|
|
128
|
+
const missingBaseline = baseline.filter((n) => !has(n) && !(n !== 'test' && bunfigDeclares(bunfig, n)))
|
|
129
|
+
// a script named like the built-in runs in its place whenever someone
|
|
130
|
+
// types `bun lint` or `bun fmt`, so a declared section makes it dead weight
|
|
131
|
+
for (const n of ['lint', 'fmt'] as const)
|
|
132
|
+
if (scripts[n] != null && bunfigDeclares(bunfig, n))
|
|
133
|
+
t.warn(`package.json: script '${n}' shadows \`bun ${n}\`, which bunfig.toml turns on with [${n}] — delete it${scripts[rawName(n)] != null ? ` and '${rawName(n)}'` : ''}`)
|
|
116
134
|
if (missingBaseline.length > 0)
|
|
117
|
-
t.warn(`package.json: missing baseline
|
|
135
|
+
t.warn(`package.json: missing baseline: ${missingBaseline.map((n) => (n === 'test' ? `script 'test'` : `[${n}] in bunfig.toml (or a '${n}' script)`)).join(', ')}`)
|
|
136
|
+
for (const r of RETIRED) {
|
|
137
|
+
// the tool as a command word: `bunx oxlint@latest`, `"oxlint;"`, never `.oxlintrc` or `oxlint-smoke.ts`
|
|
138
|
+
const word = new RegExp(`(^|[^\\w.-])${r.tool}(?![\\w-])`)
|
|
139
|
+
const sole = new RegExp(`^\\s*(?:(?:bunx|npx|bun\\s+x)\\s+)?(?:\\S*/)?${r.tool}(?:@\\S+)?(?:\\s+[^;&|]*)?$`)
|
|
140
|
+
const deps = r.deps.filter((d) => [pkg.dependencies, pkg.devDependencies, pkg.optionalDependencies].some((m) => m?.[d] != null))
|
|
141
|
+
const using = Object.entries(scripts).filter(([, body]) => word.test(body))
|
|
142
|
+
const whole = using.filter(([, body]) => sole.test(body)).map(([name]) => `'${name}'`)
|
|
143
|
+
const steps = using.filter(([, body]) => !sole.test(body)).map(([name]) => `'${name}'`)
|
|
144
|
+
const del = [
|
|
145
|
+
...deps.map((d) => `the ${d} dependency`),
|
|
146
|
+
...(whole.length > 0 ? [`script ${whole.join(', ')}`] : []),
|
|
147
|
+
...(t.exists(r.config) ? [r.config] : []),
|
|
148
|
+
]
|
|
149
|
+
if (deps.length === 0 && using.length === 0) continue
|
|
150
|
+
const parts = [...(del.length > 0 ? [`delete ${del.join(', ')}`] : []), ...(steps.length > 0 ? [`take the ${r.tool} step out of script ${steps.join(', ')}, which does more`] : [])]
|
|
151
|
+
t.warn(`package.json: ${r.tool} is retired: ${r.how} ${parts.join(', and ')}`)
|
|
152
|
+
}
|
|
118
153
|
|
|
119
154
|
if (!t.failed && !t.warned)
|
|
120
155
|
t.ok(`package.json shape ok (private, no license, ${required.length} required script(s) present)`)
|
|
@@ -182,7 +217,7 @@ const SOURCE_GLOB = '**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'
|
|
|
182
217
|
const EMPTY_MODULE = /^(?:\s|;|export\s*\{\s*\})*$/
|
|
183
218
|
|
|
184
219
|
/** a code file with something in it, outside installs, build output, dot dirs
|
|
185
|
-
* and the repo config: what lint
|
|
220
|
+
* and the repo config: what lint would read. Stops at the first */
|
|
186
221
|
function hasSourceFiles(c: CheckCtx): boolean {
|
|
187
222
|
for (const f of c.list(SOURCE_GLOB)) {
|
|
188
223
|
const parts = f.split('/')
|
|
@@ -196,7 +231,7 @@ export const packageJsonShape = check({
|
|
|
196
231
|
id: 'SK001',
|
|
197
232
|
blocking: true,
|
|
198
233
|
title: 'package.json',
|
|
199
|
-
description: 'package.json routes
|
|
234
|
+
description: 'package.json routes typecheck and test through the gate',
|
|
200
235
|
owner: OWNER,
|
|
201
236
|
runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
|
|
202
237
|
cache: { by: 'reads' },
|
|
@@ -252,7 +287,7 @@ export const packageJsonShape = check({
|
|
|
252
287
|
// call. Both keys keep their place in the file, so the diff a human
|
|
253
288
|
// reads is the two lines that changed and not a reordered block
|
|
254
289
|
for (const id of ROUTED_SCRIPTS) {
|
|
255
|
-
if (routedExempt(c.config).has(id) || c.config
|
|
290
|
+
if (routedExempt(c.config).has(id) || repoOff(c.config, id) != null) continue
|
|
256
291
|
const body = pkg.scripts[id]
|
|
257
292
|
if (body == null || routesToShipkit(body)) continue
|
|
258
293
|
const raw = rawName(id)
|
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
// scaffold, so a policy written there would bind exactly one checkout.
|
|
15
15
|
import { isAbsolute, join } from 'path'
|
|
16
16
|
import { inferBin } from '../core/canonical-scripts.ts'
|
|
17
|
-
import { denyRules, RAW_RUNNERS } from '../core/one-way-in-policy.ts'
|
|
17
|
+
import { denyRules, GIT_STASH_RULE, RAW_RUNNERS } from '../core/one-way-in-policy.ts'
|
|
18
18
|
import { rootPublishesNpm } from '../core/repo-facts.ts'
|
|
19
19
|
import type { RepoConfig } from '../repo-config.ts'
|
|
20
|
+
import { repoOff } from '../core/when.ts'
|
|
20
21
|
import { check, type CheckCtx, type FixResult } from '../check.ts'
|
|
21
22
|
|
|
22
23
|
const OWNER = 'build/shipkit/one-way-in.md'
|
|
@@ -31,7 +32,7 @@ type Settings = {
|
|
|
31
32
|
* either: ONE declaration governs the script name and the raw call together */
|
|
32
33
|
export function exemptSlots(c: RepoConfig): Set<string> {
|
|
33
34
|
const off = new Set(c.oneWayIn?.exempt ?? [])
|
|
34
|
-
for (const [
|
|
35
|
+
for (const id of [...Object.keys(c.defaults ?? {}), ...Object.keys(c.off ?? {}), ...Object.keys(c.when ?? {})]) if (repoOff(c, id) != null) off.add(id)
|
|
35
36
|
return off
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -132,14 +133,15 @@ export const oneWayInPolicy = check({
|
|
|
132
133
|
if (rootPublishesNpm(c.config)) return c.ok(PUBLIC_SKIP)
|
|
133
134
|
const d = c
|
|
134
135
|
const want = denyRules(exemptSlots(c.config))
|
|
135
|
-
if (want.length === 0) return c.ok('every governed slot is exempt (repo.config oneWayIn/defaults) — nothing to deny')
|
|
136
136
|
const s = readSettings(d)
|
|
137
137
|
if (s == null) {
|
|
138
138
|
c.fail(`${SETTINGS_PATH} ${d.exists(SETTINGS_PATH) ? 'is unparseable' : 'missing'} — nothing stops an agent typing \`bun test\`, and a package.json script cannot intercept a bun builtin`)
|
|
139
139
|
return
|
|
140
140
|
}
|
|
141
141
|
const have = new Set(s.permissions?.deny ?? [])
|
|
142
|
-
const missing = want.filter((r) => !have.has(r))
|
|
142
|
+
const missing = want.filter((r) => !have.has(r) && r !== GIT_STASH_RULE)
|
|
143
|
+
if (!have.has(GIT_STASH_RULE))
|
|
144
|
+
c.fail(`${SETTINGS_PATH}: no "${GIT_STASH_RULE}" deny row, so an agent can stash every other agent's uncommitted work in a shared tree`)
|
|
143
145
|
if (missing.length > 0)
|
|
144
146
|
c.fail(`${SETTINGS_PATH}: ${missing.length} raw runner(s) still permitted (${missing.slice(0, 3).join(', ')}${missing.length > 3 ? ', …' : ''}) — those calls never reach shipkit, so nothing is cached and no run can be attached to`)
|
|
145
147
|
const guard = guardHookCommand(s)
|
|
@@ -170,7 +172,7 @@ export const oneWayInPolicy = check({
|
|
|
170
172
|
// replacement is named while the guard cannot run is the green that
|
|
171
173
|
// measured nothing, one line under the WARN that says otherwise
|
|
172
174
|
if (!c.failed)
|
|
173
|
-
c.ok(`${SETTINGS_PATH} closes ${want.length}/${RAW_RUNNERS.length} raw runner(s)${guardRuns ? ', with the replacement named' : ' (the guard that names the replacement is declared, not yet runnable)'}`)
|
|
175
|
+
c.ok(`${SETTINGS_PATH} closes ${want.length - 1}/${RAW_RUNNERS.length} raw runner(s) and refuses git stash${guardRuns ? ', with the replacement named' : ' (the guard that names the replacement is declared, not yet runnable)'}`)
|
|
174
176
|
},
|
|
175
177
|
// MERGES, never rewrites: this file also carries the attribution silencer,
|
|
176
178
|
// a repo's own allow list and its own hooks, and clobbering those to fix a
|
|
@@ -179,7 +181,6 @@ export const oneWayInPolicy = check({
|
|
|
179
181
|
if (rootPublishesNpm(c.config)) return { changed: false, summary: PUBLIC_SKIP }
|
|
180
182
|
const d = c
|
|
181
183
|
const want = denyRules(exemptSlots(c.config))
|
|
182
|
-
if (want.length === 0) return { changed: false, summary: 'every governed slot is exempt — nothing to write' }
|
|
183
184
|
const existing = readSettings(d)
|
|
184
185
|
if (existing == null && c.exists(SETTINGS_PATH)) return { changed: false, summary: `${SETTINGS_PATH} is unparseable — repair it by hand first, a merge would destroy it` }
|
|
185
186
|
const s: Settings = existing ?? {}
|
|
@@ -188,7 +189,7 @@ export const oneWayInPolicy = check({
|
|
|
188
189
|
const added = want.filter((r) => !deny.includes(r))
|
|
189
190
|
if (added.length > 0) {
|
|
190
191
|
s.permissions = { ...s.permissions, deny: [...deny, ...added] }
|
|
191
|
-
changes.push(`${added.length}
|
|
192
|
+
changes.push(`${added.length} deny rule(s) added`)
|
|
192
193
|
}
|
|
193
194
|
const wantGuard = await guardCommandFor(d)
|
|
194
195
|
if (!hasGuardHook(s)) {
|
|
@@ -1,26 +1,24 @@
|
|
|
1
|
-
// SK017 vscode hub task (build/vscode-tasks.md):
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// that already runs `typecheck`.
|
|
1
|
+
// SK017 vscode hub task (build/vscode-tasks.md): no folder-open task starts
|
|
2
|
+
// the shipkit hub (it runs in the home, and VS Code needs no task for it), and
|
|
3
|
+
// no `👁 TSC` watcher runs beside a gate that already runs `typecheck`.
|
|
5
4
|
//
|
|
6
|
-
// the file is hand-written jsonc, so the fixer edits spans: it removes the
|
|
7
|
-
// task, a wrapper
|
|
8
|
-
//
|
|
9
|
-
//
|
|
5
|
+
// the file is hand-written jsonc, so the fixer edits spans: it removes the hub
|
|
6
|
+
// task, the TSC task, a wrapper left starting nothing, and their dependsOn
|
|
7
|
+
// entries, and folds a wrapper left starting ONE task into it. Every other
|
|
8
|
+
// byte stays, except a dependsOn array it edits, which is rewritten inline.
|
|
9
|
+
// A file left holding nothing but an empty task list is deleted.
|
|
10
10
|
import { basename } from 'path'
|
|
11
11
|
import { check } from '../check.ts'
|
|
12
12
|
import type { CheckCtx } from '../core/check-ctx.ts'
|
|
13
13
|
import { checkFloorReader, resolveFloor } from '../core/floor-resolve.ts'
|
|
14
14
|
import { rootPublishesNpm } from '../core/repo-facts.ts'
|
|
15
15
|
import { repoOff } from '../core/when.ts'
|
|
16
|
-
import {
|
|
17
|
-
import { indentAt, parseSpans, plain, removalSpan, type Node } from '../scaffold/jsonc-spans.ts'
|
|
16
|
+
import { indentAt, parseSpans, plain, removalSpans, type Node } from '../scaffold/jsonc-spans.ts'
|
|
18
17
|
|
|
19
18
|
export const TSC_LABEL = '👁 TSC'
|
|
20
19
|
const TASKS = '.vscode/tasks.json'
|
|
21
20
|
|
|
22
21
|
type Obj = Extract<Node, { kind: 'object' }>
|
|
23
|
-
type Arr = Extract<Node, { kind: 'array' }>
|
|
24
22
|
|
|
25
23
|
const prop = (o: Obj, key: string): Node | undefined => o.props.find((p) => p.key === key)?.value
|
|
26
24
|
|
|
@@ -35,12 +33,17 @@ function isTscWatcher(t: Record<string, unknown>): boolean {
|
|
|
35
33
|
return t['label'] === TSC_LABEL && TSC_BINS.has(basename(cmd)) && args.includes('--watch')
|
|
36
34
|
}
|
|
37
35
|
|
|
38
|
-
/** the hub task: `bun run hub
|
|
36
|
+
/** the hub task: `bun run hub` or `sk serve` (args or one shell string), or the house label */
|
|
39
37
|
function isHub(t: Record<string, unknown>): boolean {
|
|
40
|
-
const args = Array.isArray(t['args']) ? t['args'] : []
|
|
41
|
-
|
|
38
|
+
const args = Array.isArray(t['args']) ? t['args'].map(String) : []
|
|
39
|
+
const line = [typeof t['command'] === 'string' ? t['command'] : '', ...args].join(' ')
|
|
40
|
+
return /(?:^|\s)(?:bun run hub|(?:sk|shipkit) serve)(?:\s|$)/.test(line) || (typeof t['label'] === 'string' && /^🚀\s+shipkit\b/.test(t['label']))
|
|
42
41
|
}
|
|
43
42
|
|
|
43
|
+
/** the comment the old scaffold wrote above its hub task, byte for byte, any
|
|
44
|
+
* indent: it describes the hub task, so it goes with it */
|
|
45
|
+
const SCAFFOLD_HEADER = /^[ \t]*\/\/ the hub is the one process every wired repo starts, so it opens with the\n[ \t]*\/\/ folder itself\. It runs every check after each save and its problem\n[ \t]*\/\/ matcher fills the problems pane, so there is no type checker task\. A\n[ \t]*\/\/ stack that earns more long-running watchers \(dev server, css\) adds a\n[ \t]*\/\/ master task with runOn folderOpen whose dependsOn lists them all, the hub\n[ \t]*\/\/ included, and moves runOn off the hub\.\n/m
|
|
46
|
+
|
|
44
47
|
/** a task that only starts other tasks: no command of its own */
|
|
45
48
|
const isWrapper = (t: Record<string, unknown>): boolean => t['command'] == null && t['type'] == null && t['dependsOn'] != null
|
|
46
49
|
|
|
@@ -50,112 +53,111 @@ export type RepairOpts = {
|
|
|
50
53
|
typecheckOn: boolean
|
|
51
54
|
}
|
|
52
55
|
|
|
53
|
-
/** what this file needs, applied: the text to write, one line per change,
|
|
54
|
-
* one line per thing it deliberately left
|
|
55
|
-
* back byte for byte */
|
|
56
|
-
export function repairTasks(text: string, o: RepairOpts): { text: string; changes: string[]; kept: string[] } {
|
|
56
|
+
/** what this file needs, applied: the text to write, one line per change,
|
|
57
|
+
* one line per thing it deliberately left, and whether any task is left. No
|
|
58
|
+
* change means the text comes back byte for byte */
|
|
59
|
+
export function repairTasks(text: string, o: RepairOpts): { text: string; changes: string[]; kept: string[]; empty: boolean } {
|
|
57
60
|
const root = parseSpans(text)
|
|
58
61
|
if (root.kind !== 'object') throw new Error('tasks.json is not an object')
|
|
59
62
|
const tasks = prop(root, 'tasks')
|
|
60
|
-
if (tasks == null || tasks.kind !== 'array') return { text, changes: [], kept: [] }
|
|
61
|
-
const edits: { start: number; end: number; body: string }[] = []
|
|
63
|
+
if (tasks == null || tasks.kind !== 'array') return { text, changes: [], kept: [], empty: false }
|
|
62
64
|
const changes: string[] = []
|
|
63
65
|
const kept: string[] = []
|
|
64
66
|
const record = (t: Node): Record<string, unknown> => plain(text, t) as Record<string, unknown>
|
|
65
67
|
const labelOf = (t: Node): string => String(record(t)['label'] ?? '?')
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
edits.push({ start, end, body: '' })
|
|
75
|
-
changes.push(`removed the '${TSC_LABEL}' task`)
|
|
68
|
+
const at = (pred: (t: Record<string, unknown>) => boolean): number[] => tasks.items.flatMap((n, i) => (n.kind === 'object' && pred(record(n)) ? [i] : []))
|
|
69
|
+
|
|
70
|
+
const remove = new Set<number>()
|
|
71
|
+
const gone = new Set<string>()
|
|
72
|
+
const drop = (i: number, why: string): void => {
|
|
73
|
+
remove.add(i)
|
|
74
|
+
gone.add(labelOf(tasks.items[i]!))
|
|
75
|
+
changes.push(why)
|
|
76
76
|
}
|
|
77
|
+
for (const i of at(isHub)) drop(i, `removed the '${labelOf(tasks.items[i]!)}' hub task: the hub runs in the home, not in VS Code`)
|
|
78
|
+
const tscAt = at(isTscWatcher)
|
|
79
|
+
if (tscAt.length > 0 && !o.typecheckOn) kept.push(`kept the '${TSC_LABEL}' task: this repo's gate has no enabled typecheck check, so it is the only type checker here`)
|
|
80
|
+
if (o.typecheckOn) for (const i of tscAt) drop(i, `removed the '${TSC_LABEL}' task`)
|
|
77
81
|
|
|
78
|
-
// a folder-open wrapper whose only job is starting the hub is a second
|
|
79
|
-
// task on screen for nothing: the hub opens with the folder itself
|
|
80
|
-
const hub = hubs.length === 1 ? hubs[0]! : null
|
|
81
|
-
const hubLabel = hub == null ? null : labelOf(hub)
|
|
82
82
|
const depsOf = (t: Node): string[] => {
|
|
83
83
|
const d = record(t)['dependsOn']
|
|
84
|
-
|
|
85
|
-
return dropTsc ? list.filter((x) => x !== TSC_LABEL) : list
|
|
84
|
+
return d == null ? [] : Array.isArray(d) ? d.map(String) : [String(d)]
|
|
86
85
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// a wrapper that started only the TSC task starts nothing once it goes
|
|
97
|
-
const emptied = !dropTsc ? [] : tasks.items.flatMap((n, i) => (n !== wrapper && n.kind === 'object' && isWrapper(record(n)) && depsOf(n).length === 0 ? [i] : []))
|
|
98
|
-
for (const i of emptied) {
|
|
99
|
-
const [start, end] = removalSpan(text, tasks, i)
|
|
100
|
-
edits.push({ start, end, body: '' })
|
|
101
|
-
changes.push(`removed the '${labelOf(tasks.items[i]!)}' wrapper, which started nothing else`)
|
|
86
|
+
// a wrapper that started only what went starts nothing, and so on up
|
|
87
|
+
for (let moved = true; moved;) {
|
|
88
|
+
moved = false
|
|
89
|
+
tasks.items.forEach((n, i) => {
|
|
90
|
+
if (remove.has(i) || n.kind !== 'object' || !isWrapper(record(n))) return
|
|
91
|
+
const deps = depsOf(n)
|
|
92
|
+
if (deps.length > 0 && deps.every((x) => gone.has(x))) { drop(i, `removed the '${labelOf(n)}' wrapper, which started nothing else`); moved = true }
|
|
93
|
+
})
|
|
102
94
|
}
|
|
103
95
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
const
|
|
96
|
+
const edits: { start: number; end: number; body: string }[] = []
|
|
97
|
+
// a wrapper this pass cut down to ONE task is two tasks on screen for one
|
|
98
|
+
// process: it folds into that task, which takes the wrapper's own runOn
|
|
99
|
+
// and group, and every dependsOn naming the wrapper names the task instead
|
|
100
|
+
const alias = new Map<string, string>()
|
|
101
|
+
const folds = new Map<Obj, Obj[]>()
|
|
102
|
+
tasks.items.forEach((n, i) => {
|
|
103
|
+
if (remove.has(i) || n.kind !== 'object' || !isWrapper(record(n))) return
|
|
104
|
+
const deps = depsOf(n)
|
|
105
|
+
const left = deps.filter((x) => !gone.has(x))
|
|
106
|
+
if (left.length !== 1 || left.length === deps.length) return
|
|
107
|
+
const into = tasks.items.findIndex((t, j) => !remove.has(j) && t.kind === 'object' && labelOf(t) === left[0])
|
|
108
|
+
if (into < 0) return
|
|
109
|
+
remove.add(i)
|
|
110
|
+
alias.set(labelOf(n), left[0]!)
|
|
111
|
+
folds.set(tasks.items[into] as Obj, [...(folds.get(tasks.items[into] as Obj) ?? []), n])
|
|
112
|
+
const opens = (record(n)['runOptions'] as Record<string, unknown> | undefined)?.['runOn'] === 'folderOpen'
|
|
113
|
+
changes.push(`folded the '${labelOf(n)}' wrapper into '${left[0]}'${opens ? ', which now opens with the folder' : ''}`)
|
|
114
|
+
})
|
|
115
|
+
tasks.items.forEach((n, i) => {
|
|
116
|
+
if (remove.has(i) || n.kind !== 'object') return
|
|
117
|
+
const d = prop(n, 'dependsOn')
|
|
118
|
+
const deps = depsOf(n)
|
|
119
|
+
const next = [...new Set(deps.map((x) => alias.get(x) ?? x).filter((x) => !gone.has(x) && x !== labelOf(n)))]
|
|
120
|
+
if (d == null || JSON.stringify(next) === JSON.stringify(deps)) return
|
|
121
|
+
edits.push({ start: d.start, end: d.end, body: JSON.stringify(next).replace(/","/g, '", "') })
|
|
122
|
+
changes.push(`dropped ${deps.filter((x) => !next.includes(x)).map((x) => `'${x}'`).join(', ')} from the dependsOn of '${labelOf(n)}'`)
|
|
123
|
+
})
|
|
124
|
+
for (const [start, end] of removalSpans(text, tasks, remove)) edits.push({ start, end, body: '' })
|
|
125
|
+
// the default build group a removed hub held moves with the fold too
|
|
126
|
+
const hubGroup = at(isHub).map((i) => prop(tasks.items[i] as Obj, 'group')).find((g) => g != null && /"isDefault"\s*:\s*true/.test(text.slice(g.start, g.end)))
|
|
127
|
+
for (const [task, wrappers] of folds) {
|
|
128
|
+
const first = task.props[0]
|
|
129
|
+
const taskIndent = indentAt(text, task.start)
|
|
130
|
+
const propIndent = first != null ? indentAt(text, first.keyStart) : `${taskIndent} `
|
|
128
131
|
const added: string[] = []
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
else added.push(`"problemMatcher": ${render(want)}`)
|
|
133
|
-
changes.push(`wrote the shipkit problem matcher on '${labelOf(h)}'`)
|
|
134
|
-
}
|
|
135
|
-
if (h === hub && wrapper != null) {
|
|
136
|
-
const ro = prop(h, 'runOptions')
|
|
132
|
+
const runOn = wrappers.map((w) => record(w)['runOptions'] as Record<string, unknown> | undefined).find((r) => r?.['runOn'] != null)?.['runOn']
|
|
133
|
+
if (runOn != null) {
|
|
134
|
+
const ro = prop(task, 'runOptions')
|
|
137
135
|
const was = ro != null && ro.kind === 'object' ? record(ro) : {}
|
|
138
|
-
const inline = `{ ${Object.entries(
|
|
136
|
+
const inline = `{ ${Object.entries({ ...was, runOn, instanceLimit: was['instanceLimit'] ?? 1 }).map(([k, v]) => `${JSON.stringify(k)}: ${JSON.stringify(v)}`).join(', ')} }`
|
|
139
137
|
if (ro != null) edits.push({ start: ro.start, end: ro.end, body: inline })
|
|
140
138
|
else added.push(`"runOptions": ${inline}`)
|
|
141
|
-
const wg = prop(wrapper, 'group')
|
|
142
|
-
if (wg != null) {
|
|
143
|
-
const body = text.slice(wg.start, wg.end)
|
|
144
|
-
const g = prop(h, 'group')
|
|
145
|
-
if (g != null) edits.push({ start: g.start, end: g.end, body })
|
|
146
|
-
else added.push(`"group": ${body}`)
|
|
147
|
-
}
|
|
148
139
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
140
|
+
const wg = wrappers.map((w) => prop(w, 'group')).find((g) => g != null) ?? hubGroup
|
|
141
|
+
if (wg != null) {
|
|
142
|
+
const body = text.slice(wg.start, wg.end)
|
|
143
|
+
const g = prop(task, 'group')
|
|
144
|
+
if (g != null) edits.push({ start: g.start, end: g.end, body })
|
|
145
|
+
else added.push(`"group": ${body}`)
|
|
146
|
+
}
|
|
147
|
+
const last = task.props.at(-1)
|
|
148
|
+
if (added.length > 0 && last != null) edits.push({ start: last.value.end, end: last.value.end, body: `,\n${propIndent}${added.join(`,\n${propIndent}`)}` })
|
|
154
149
|
}
|
|
155
150
|
|
|
151
|
+
const header = gone.size > 0 ? SCAFFOLD_HEADER.exec(text) : null
|
|
152
|
+
if (header != null) {
|
|
153
|
+
edits.push({ start: header.index, end: header.index + header[0].length, body: '' })
|
|
154
|
+
changes.push('removed the old scaffold comment that described the hub task')
|
|
155
|
+
}
|
|
156
156
|
let out = text
|
|
157
157
|
for (const e of edits.sort((a, b) => b.start - a.start)) out = out.slice(0, e.start) + e.body + out.slice(e.end)
|
|
158
|
-
|
|
158
|
+
// empty only when nothing else lives in the file either (`inputs`, a key of its own)
|
|
159
|
+
const onlyTasks = root.props.every((x) => x.key === 'version' || x.key === 'tasks')
|
|
160
|
+
return { text: out, changes, kept, empty: onlyTasks && tasks.items.length > 0 && remove.size === tasks.items.length }
|
|
159
161
|
}
|
|
160
162
|
|
|
161
163
|
/** whether this repo's gate runs a typecheck: a floor slot or a task that
|
|
@@ -184,26 +186,26 @@ const PUBLIC_SKIP = `the root unit publishes to npm: ${TASKS} belongs to every c
|
|
|
184
186
|
export const vscodeHubTask = check({
|
|
185
187
|
id: 'SK017',
|
|
186
188
|
// a stale editor file is behind, not broken: it warns, and the fix pass
|
|
187
|
-
// repairs it
|
|
189
|
+
// of every gate repairs it
|
|
188
190
|
blocking: false,
|
|
189
191
|
title: 'vscode hub task',
|
|
190
|
-
description: '
|
|
192
|
+
description: 'no vscode task starts the shipkit hub, and no extra tsc watcher runs',
|
|
191
193
|
owner: 'build/vscode-tasks.md',
|
|
192
|
-
runs: { onPreCommit: false, onCi:
|
|
194
|
+
runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: false, onHealth: true, onSave: 'moved', onClock: false },
|
|
193
195
|
cache: { by: 'reads' },
|
|
194
|
-
fix: '
|
|
196
|
+
fix: 'ci',
|
|
195
197
|
applies: 'always',
|
|
196
198
|
run: (c) => {
|
|
197
199
|
if (rootPublishesNpm(c.config)) return c.ok(PUBLIC_SKIP)
|
|
198
200
|
const own = ownTasks(c)
|
|
199
|
-
if (own === 'none') return c.ok(`no ${TASKS}, so
|
|
201
|
+
if (own === 'none') return c.ok(`no ${TASKS}, so no task starts the hub`)
|
|
200
202
|
if (own === 'linked') return c.ok(`.vscode/ is a link to a shared dir, left alone`)
|
|
201
203
|
let r: { changes: string[]; kept: string[] }
|
|
202
204
|
try { r = repairTasks(own.text, { typecheckOn: typecheckOn(c) }) }
|
|
203
205
|
catch (e) { return c.warn(`${TASKS} does not parse (${(e as Error).message}), so the hub task was not checked`) }
|
|
204
206
|
for (const k of r.kept) c.note(`${TASKS}: ${k}`)
|
|
205
|
-
if (r.changes.length === 0) return c.ok(`${TASKS}:
|
|
206
|
-
for (const ch of r.changes) c.fail(`${TASKS}: ${ch.replace(/^removed/, 'should remove').replace(/^dropped/, 'should drop')
|
|
207
|
+
if (r.changes.length === 0) return c.ok(`${TASKS}: no task starts the hub`)
|
|
208
|
+
for (const ch of r.changes) c.fail(`${TASKS}: ${ch.replace(/^removed/, 'should remove').replace(/^dropped/, 'should drop')}`)
|
|
207
209
|
},
|
|
208
210
|
repair: (c) => {
|
|
209
211
|
if (rootPublishesNpm(c.config)) return { changed: false, summary: PUBLIC_SKIP }
|
|
@@ -211,8 +213,9 @@ export const vscodeHubTask = check({
|
|
|
211
213
|
if (typeof own === 'string') return { changed: false, summary: `no ${TASKS} to repair` }
|
|
212
214
|
const r = repairTasks(own.text, { typecheckOn: typecheckOn(c) })
|
|
213
215
|
if (r.changes.length === 0) return { changed: false, summary: `${TASKS} already current` }
|
|
214
|
-
c.
|
|
215
|
-
|
|
216
|
+
if (r.empty) c.remove(TASKS)
|
|
217
|
+
else c.write(TASKS, r.text)
|
|
218
|
+
return { changed: true, summary: `${TASKS}: ${r.changes.join(', ')}${r.empty ? ', and deleted the file, which held no other task' : ''}` }
|
|
216
219
|
},
|
|
217
220
|
})
|
|
218
221
|
|