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
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,69 @@
|
|
|
1
|
+
## 2.0.0
|
|
2
|
+
|
|
3
|
+
- **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.
|
|
4
|
+
- **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.
|
|
5
|
+
- **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.
|
|
6
|
+
- **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.
|
|
7
|
+
- **`sk commands --compact`** renders one line per command, for a page every session loads.
|
|
8
|
+
- **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`.
|
|
9
|
+
- **`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.
|
|
10
|
+
- **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.
|
|
11
|
+
- **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.
|
|
12
|
+
- **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.
|
|
13
|
+
- **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>`.
|
|
14
|
+
- **`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.
|
|
15
|
+
- **`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.
|
|
16
|
+
- **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.
|
|
17
|
+
- **`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)`.
|
|
18
|
+
- **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.
|
|
19
|
+
- **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.
|
|
20
|
+
- **`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.
|
|
21
|
+
- **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.
|
|
22
|
+
- **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.
|
|
23
|
+
- **`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.
|
|
24
|
+
- **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.
|
|
25
|
+
- **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.
|
|
26
|
+
- **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.
|
|
27
|
+
- **A canary upgrade invalidates cached answers.** The toolchain identity hashes `Bun.revision` beside `Bun.version`, which every canary of one version shares.
|
|
28
|
+
- **`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.
|
|
29
|
+
- **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`.
|
|
30
|
+
- **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>`.
|
|
31
|
+
|
|
32
|
+
## 1.9.0
|
|
33
|
+
|
|
34
|
+
- **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.
|
|
35
|
+
- **A broken built-in check no longer aborts every repo's gate.** In a repo that only consumes the toolkit, a built-in that does not load or is refused is one non-blocking WARN that names the file. The repo that hosts the toolkit source still fails on it. Before, it threw a misleading "names the page that OWNS the invariant" error before any check ran.
|
|
36
|
+
- **`sk gp open <id>` probes before it trusts "up".** A process that died since pm last looked now starts before its window opens, instead of a window on a dead port.
|
|
37
|
+
- **A linked worktree of a house repo is no longer a house repo of its own.** The home, the global context and pm skip a sibling whose `.git` is a file, so its global processes no longer collide with the main checkout's ids and ports.
|
|
38
|
+
- **`sk ci --only <check>` leaves the tree alone.** A narrowed run no longer runs the fix pass unless `--fix` is named, so `bun run test`, `lint` and `typecheck`, which route through `--only`, never reformat a shared tree. Before, the formatter ran on every narrowed run and rewrote 478 files of one checkout, other agents' uncommitted work included. A whole gate still repairs first, and `--no-fix` still turns it off.
|
|
39
|
+
|
|
40
|
+
- **The machine pool admits fewer checks on a loaded machine.** A slot is a check, and a check is often a whole multi core tool, so eight slots on eight cores could put several times the cores to work. Past a 1 minute load of one per core the pool now admits slots in proportion to the load, never fewer than one: at load 20 on 8 cores it admits 3 of 8. A slot already held is never taken back. A waiting check prints the load and what the pool admits.
|
|
41
|
+
|
|
42
|
+
- **The plan holds save runs while someone gates the repo.** A check a save moved now also waits 10 minutes after the latest run a terminal, an agent or a click started in its repo. On one repo with several agents, most of the plan's test runs had started within those 10 minutes and measured a tree the agents were gating themselves. The run is held, not dropped, so the tree still gets one verdict once they stop. `sk planned` shows the hold as a `deferred:` line.
|
|
43
|
+
- **A suite over the CPU budget in its lone run runs alone once more, and the smaller figure stands.** On a busy Apple silicon machine the scheduler can spill a lone run onto the efficiency cores, where the same work burns up to 4x the CPU time, so a suite inside its budget failed one run in three and passed a minute later. A spill only raises CPU time, so the second run can clear a false red and never hide a real one. A wait or a suite with no CPU figure keeps its one run, and a green gate pays nothing.
|
|
44
|
+
|
|
45
|
+
- **The gate's test line says the command the suite runner ran.** A plain `bun test` body does not run as written under `sk ci`: the runner adds `--parallel=<n>` and `--no-isolate`. The line now reads `ok: test 412 tests (ran 1.5s) as \`bun test --parallel=7 --no-isolate\` (added --parallel=7 --no-isolate)`, so a reader of package.json knows that `bun run test` outside the gate runs another way. Per file ignores stay out of it.
|
|
46
|
+
|
|
47
|
+
- **A test phase killed at its ceiling names the suite that hung, and both things that hang a worker.** bun lists the suites still running when a phase is killed, and the parallel phase now reads that list, so the line says `with tests/x.test.ts still running`. It names a synchronous loop and `expect(p).resolves` or `.rejects` on a promise that never settles: bun's per test timeout stops neither, while a plain `await` of such a promise fails at the per test timeout. And a red suite is no longer judged on time: its time is its failure's, so a test that timed out no longer runs again alone and no longer adds a budget FAIL.
|
|
48
|
+
- **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`.
|
|
49
|
+
|
|
50
|
+
- **`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.
|
|
51
|
+
- **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.
|
|
52
|
+
- **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.
|
|
53
|
+
- **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.
|
|
54
|
+
|
|
55
|
+
- **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.
|
|
56
|
+
|
|
57
|
+
- **`sk assets gen` works on a repo whose comfy-ts is newer than shipkit's.** The one comfy-ts instance of the process is now created through the copy the repo's workflow modules import, so a module gets the vars builder of its own version. Before, shipkit's older copy rooted it, and a module using a newer var kind failed every roll (`v.model is not a function`).
|
|
58
|
+
|
|
59
|
+
- **The og card draws accented names.** The default card names a font family list (Helvetica, Arial, DejaVu Sans, Liberation Sans, Noto Sans, then sans-serif). The generic sans-serif alone resolved on macOS to a face with no Latin-1 accents, so a French name rendered with a missing glyph box.
|
|
60
|
+
|
|
61
|
+
- **`sk smoke` serves canvas apps: a route takes `minChars`**, the visible text a settled route needs (default 40). A game that draws into one canvas declares `{ path: '/', waitFor: 'canvas', minChars: 0 }`, and a missing canvas still fails the route.
|
|
62
|
+
|
|
63
|
+
- **`serve: { assets: false }` hides the asset kit from the hub page and from `sk needs`**, for a repo whose own tool mounts `studioRoutes` and owns the picking. Workflows and posts stay.
|
|
64
|
+
|
|
65
|
+
- **`defineKit({ rollFormat: 'webp-lossless' })` stores new image rolls as lossless webp**, the same pixels at about 60 percent of the png bytes, encoded with sharp. Existing png rolls stay readable beside them.
|
|
66
|
+
|
|
1
67
|
## 1.8.0
|
|
2
68
|
|
|
3
69
|
- **The quick tier can run only the suites a change reaches** (opt in: `tests: { select: true }`, or `SHIPKIT_TEST_SELECT=1` for one run). It keeps the snapshot of the last run and its red suites, and runs those plus every suite whose import closure, or whose recorded runtime reads, a change since then reaches. What it cannot see it runs: an unclaimed change under a code root, a spawn it does not know, a module that threw while loading, a run config file. The full tier still runs every suite.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rvlib-shipkit",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
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",
|
|
@@ -40,6 +40,19 @@ const varsOf = (wf: DefinedWorkflow): Record<string, AnyVar> => wf.vars as Recor
|
|
|
40
40
|
export const isRollImage = (img: Pick<MediaImage, 'ComfyNode'>): boolean =>
|
|
41
41
|
!/preview/i.test(img.ComfyNode?.class_type ?? '')
|
|
42
42
|
|
|
43
|
+
/** the ComfyTS class of the comfy-ts a bare import from `dir` reaches,
|
|
44
|
+
* shipkit's own when the repo carries none */
|
|
45
|
+
async function comfyTsOf(dir: string): Promise<typeof ComfyTS> {
|
|
46
|
+
let path: string
|
|
47
|
+
try {
|
|
48
|
+
path = Bun.resolveSync('comfy-ts', dir)
|
|
49
|
+
} catch {
|
|
50
|
+
return ComfyTS
|
|
51
|
+
}
|
|
52
|
+
const mod = (await import(path)) as { ComfyTS?: typeof ComfyTS }
|
|
53
|
+
return typeof mod.ComfyTS?.create === 'function' ? mod.ComfyTS : ComfyTS
|
|
54
|
+
}
|
|
55
|
+
|
|
43
56
|
/** dir = the flat folder holding the <id>.cflow.ts workflow modules.
|
|
44
57
|
* root pins `.comfy-ts/` (drafts, hosts cache) to the repo, cwd-independent. */
|
|
45
58
|
export async function loadCflowWorkflows(root: string, dir: string): Promise<Map<string, LoadedWorkflow>> {
|
|
@@ -51,9 +64,12 @@ export async function loadCflowWorkflows(root: string, dir: string): Promise<Map
|
|
|
51
64
|
// module then returns this same instance. comfy-ts keeps ONE instance per
|
|
52
65
|
// process, and the daemon serves every repo from one process, so only the
|
|
53
66
|
// first repo roots it. What it roots is the host caches, shared by every
|
|
54
|
-
// repo on the same host; each repo's drafts resolve from its own root
|
|
55
|
-
|
|
67
|
+
// repo on the same host; each repo's drafts resolve from its own root.
|
|
68
|
+
// The instance is created through the copy the modules import, never
|
|
69
|
+
// shipkit's own: a host hands its vars builder to defineWorkflow, so an
|
|
70
|
+
// older copy gives a newer module a v without its newer var kinds
|
|
56
71
|
if (!existsSync(dir)) fail(`${dir}/ missing — ship at least one <id>.cflow.ts comfy-ts workflow module (copy from the rv-comfy template)`)
|
|
72
|
+
if ((globalThis as { comfyts?: unknown }).comfyts == null) (await comfyTsOf(dir)).create({ rootPath: root })
|
|
57
73
|
const out = new Map<string, LoadedWorkflow>()
|
|
58
74
|
for (const f of readdirSync(dir).filter((x) => x.endsWith('.cflow.ts')).sort()) {
|
|
59
75
|
const file = join(dir, f)
|
|
@@ -65,24 +81,25 @@ export async function loadCflowWorkflows(root: string, dir: string): Promise<Map
|
|
|
65
81
|
// so letting them diverge would strand every per-asset draft
|
|
66
82
|
if (wf.spec.id != null && wf.spec.id !== id) fail(`workflow file '${f}' declares id '${wf.spec.id}' — the file name IS the id, rename one`)
|
|
67
83
|
wf.spec.id ??= id
|
|
68
|
-
const vars = varsOf(wf)
|
|
69
|
-
if (vars.prompt?.kind !== 'prompt' || vars.seed?.kind !== 'seed')
|
|
70
|
-
fail(`workflow '${id}' must declare vars.prompt (v.prompt) and vars.seed (v.seed) — shipkit drives both`)
|
|
71
84
|
out.set(id, { id, root, file, wf })
|
|
72
85
|
}
|
|
73
86
|
if (out.size === 0) fail(`${dir}/ has no *.cflow.ts workflow modules — seed one from the rv-comfy template`)
|
|
74
87
|
return out
|
|
75
88
|
}
|
|
76
89
|
|
|
77
|
-
/** Resolve a
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
90
|
+
/** Resolve the workflow a kit kind drives: explicit id, else the sole loaded
|
|
91
|
+
* workflow. The atelier writes prompt and seed, so the contract binds here,
|
|
92
|
+
* never at folder load: a workflow no kind drives names its vars freely. */
|
|
93
|
+
export function workflowFor(workflows: Map<string, LoadedWorkflow>, ref: { workflow?: string; kind?: string; assetId?: string }): LoadedWorkflow {
|
|
94
|
+
const lw = ref.workflow != null
|
|
95
|
+
? (workflows.get(ref.workflow) ?? fail(`asset '${ref.assetId ?? '?'}': unknown workflow '${ref.workflow}' (have: ${[...workflows.keys()].join(', ')})`))
|
|
96
|
+
: workflows.size === 1
|
|
97
|
+
? [...workflows.values()][0]!
|
|
98
|
+
: fail(`asset '${ref.assetId ?? '?'}' names no workflow and ${workflows.size} are loaded (${[...workflows.keys()].join(', ')}) — set workflow on the kind`)
|
|
99
|
+
const vars = varsOf(lw.wf)
|
|
100
|
+
if (vars.prompt?.kind !== 'prompt' || vars.seed?.kind !== 'seed')
|
|
101
|
+
fail(`kind '${ref.kind ?? '?'}' drives workflow '${lw.id}', which must declare vars.prompt (v.prompt) and vars.seed (v.seed) — the atelier writes both`)
|
|
102
|
+
return lw
|
|
86
103
|
}
|
|
87
104
|
|
|
88
105
|
/** Apply a name→value record onto a workflow's vars: strings go through
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
// in both (before this, canvas draw lived only in the studio and the CLI
|
|
5
5
|
// refused canvas kinds — A30). Comfy context is resolved LAZILY: canvas-only
|
|
6
6
|
// kits never touch a comfy config or the box.
|
|
7
|
-
import { mkdirSync } from 'fs'
|
|
7
|
+
import { mkdirSync, unlinkSync } from 'fs'
|
|
8
8
|
import { join } from 'path'
|
|
9
|
-
import type { FoundAsset } from '../kit-config.ts'
|
|
9
|
+
import type { FoundAsset, RollFormat } from '../kit-config.ts'
|
|
10
10
|
import { generateComfyTs, loadCflowWorkflows, workflowFor, type ComfyTsRuntime, type OnProgress } from './comfyts-gen.ts'
|
|
11
11
|
import { recordParents, type ParentContext } from './derived.ts'
|
|
12
12
|
import { generateGemini } from './gemini-gen.ts'
|
|
@@ -16,11 +16,22 @@ export const CANVAS_SIZE = 1024
|
|
|
16
16
|
|
|
17
17
|
export type ComfyResolver = () => Promise<ComfyTsRuntime>
|
|
18
18
|
|
|
19
|
+
/** re-encode freshly written png rolls in the kit's roll format, before any
|
|
20
|
+
* sidecar names them. `exact` keeps the colour under transparent pixels */
|
|
21
|
+
async function encodeRolls(saved: string[], format: RollFormat | undefined): Promise<void> {
|
|
22
|
+
if (format !== 'webp-lossless') return
|
|
23
|
+
const { default: sharp } = await import('sharp')
|
|
24
|
+
for (const png of saved.filter((p) => p.endsWith('.png'))) {
|
|
25
|
+
await sharp(png).webp({ lossless: true, exact: true }).toFile(png.replace(/\.png$/, '.webp'))
|
|
26
|
+
unlinkSync(png)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
19
30
|
/** Draw/generate one roll for `found` into destDir as v<seed>.png (canvas) or
|
|
20
31
|
* v<seed>.<ext> (comfy). `resolveComfy` is only awaited for comfy backends. */
|
|
21
32
|
export async function generateRoll(
|
|
22
33
|
found: FoundAsset,
|
|
23
|
-
args: { prompt: string; seed: number; destDir: string; onProgress?: OnProgress; context?: ParentContext },
|
|
34
|
+
args: { prompt: string; seed: number; destDir: string; onProgress?: OnProgress; context?: ParentContext; rollFormat?: RollFormat },
|
|
24
35
|
resolveComfy: ComfyResolver,
|
|
25
36
|
): Promise<void> {
|
|
26
37
|
const gen = found.kind.generate
|
|
@@ -31,7 +42,7 @@ export async function generateRoll(
|
|
|
31
42
|
// resolves them (queue/CLI) so a blocker names the parent, loud
|
|
32
43
|
if ((found.asset.from?.length ?? 0) > 0 && args.context == null)
|
|
33
44
|
throw new Error(`'${found.asset.id}' derives from ${found.asset.from!.join(', ')} — parent context missing (are the parents picked?)`)
|
|
34
|
-
await generateGemini({}, { prompt: args.prompt, model: gen.model, destDir: args.destDir, name: `v${args.seed}`, context: args.context?.images ?? [] })
|
|
45
|
+
await encodeRolls(await generateGemini({}, { prompt: args.prompt, model: gen.model, destDir: args.destDir, name: `v${args.seed}`, context: args.context?.images ?? [] }), args.rollFormat)
|
|
35
46
|
if (args.context != null) await recordParents(args.destDir, args.seed, args.context.parents)
|
|
36
47
|
return
|
|
37
48
|
}
|
|
@@ -40,12 +51,14 @@ export async function generateRoll(
|
|
|
40
51
|
const c = createCanvas(CANVAS_SIZE, CANVAS_SIZE)
|
|
41
52
|
await gen.draw(c.getContext('2d'), { ...found.asset, prompt: args.prompt })
|
|
42
53
|
mkdirSync(args.destDir, { recursive: true })
|
|
43
|
-
|
|
54
|
+
const png = join(args.destDir, `v${args.seed}.png`)
|
|
55
|
+
await Bun.write(png, c.toBuffer('image/png'))
|
|
56
|
+
await encodeRolls([png], args.rollFormat)
|
|
44
57
|
return
|
|
45
58
|
}
|
|
46
59
|
const rt = await resolveComfy()
|
|
47
|
-
const lw = workflowFor(rt.workflows, { workflow: gen.workflow, assetId: found.asset.id })
|
|
48
|
-
await generateComfyTs(rt, lw, { prompt: args.prompt, seed: args.seed, assetId: found.asset.id, draftId: found.kind.id, destDir: args.destDir, name: `v${args.seed}`, vars: gen.vars, onProgress: args.onProgress })
|
|
60
|
+
const lw = workflowFor(rt.workflows, { workflow: gen.workflow, kind: found.kind.id, assetId: found.asset.id })
|
|
61
|
+
await encodeRolls(await generateComfyTs(rt, lw, { prompt: args.prompt, seed: args.seed, assetId: found.asset.id, draftId: found.kind.id, destDir: args.destDir, name: `v${args.seed}`, vars: gen.vars, onProgress: args.onProgress }), args.rollFormat)
|
|
49
62
|
}
|
|
50
63
|
|
|
51
64
|
/** Memoized lazy comfy runtime for a resolved kit — loads the cflow modules on
|
package/src/assets/kit-config.ts
CHANGED
|
@@ -127,14 +127,23 @@ export type KitConfig = {
|
|
|
127
127
|
/** workflows = dir of comfy-ts .cflow.ts modules (each module owns its
|
|
128
128
|
* host); heal = shell command shown in error hints, never executed */
|
|
129
129
|
comfy?: { workflows: string; heal?: string }
|
|
130
|
+
/** absent: an image roll lands in the format its backend produced (png
|
|
131
|
+
* from comfy and canvas). 'webp-lossless' re-encodes every new image
|
|
132
|
+
* roll as lossless webp, the same pixels at about 60% of the bytes */
|
|
133
|
+
rollFormat?: RollFormat
|
|
130
134
|
kinds: Kind[]
|
|
131
135
|
}
|
|
132
136
|
|
|
137
|
+
export const ROLL_FORMATS = ['webp-lossless'] as const
|
|
138
|
+
export type RollFormat = (typeof ROLL_FORMATS)[number]
|
|
139
|
+
|
|
133
140
|
export type ResolvedKit = KitConfig & { workspace: string }
|
|
134
141
|
|
|
135
142
|
export function validateKit(k: KitConfig): string[] {
|
|
136
143
|
const errs: string[] = []
|
|
137
144
|
if (!Array.isArray(k.kinds) || k.kinds.length === 0) errs.push('kinds must be a non-empty array')
|
|
145
|
+
if (k.rollFormat != null && !(ROLL_FORMATS as readonly string[]).includes(k.rollFormat))
|
|
146
|
+
errs.push(`unknown rollFormat '${String(k.rollFormat)}' (${ROLL_FORMATS.join(' | ')}, or absent for the backend's own format)`)
|
|
138
147
|
for (const key of Object.keys(k.comfy ?? {}))
|
|
139
148
|
if (key !== 'workflows' && key !== 'heal')
|
|
140
149
|
errs.push(`comfy config has unknown key '${key}'${key === 'host' ? " — host is gone, the workflow's _host.ts owns the box (comfy-ts migration)" : ''}`)
|
|
@@ -51,10 +51,16 @@ export function identityKinds(brand: Brand): Kind[] {
|
|
|
51
51
|
]
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// named faces first: the generic sans-serif can resolve to a face with no
|
|
55
|
+
// Latin-1 accents (macOS). Each family below carries them, and an absent one
|
|
56
|
+
// is skipped (macOS, Windows, then the common Linux faces)
|
|
57
|
+
const OG_FAMILIES = 'Helvetica, Arial, "DejaVu Sans", "Liberation Sans", "Noto Sans", sans-serif'
|
|
58
|
+
export const ogFont = (px: number, bold = false): string => `${bold ? 'bold ' : ''}${px}px ${OG_FAMILIES}`
|
|
59
|
+
|
|
54
60
|
// shrink px until text fits maxW (og cards must never clip the tagline)
|
|
55
61
|
const fitFont = (ctx: SKRSContext2D, text: string, px: number, maxW: number, bold = false): void => {
|
|
56
62
|
for (; px > 12; px -= 2) {
|
|
57
|
-
ctx.font =
|
|
63
|
+
ctx.font = ogFont(px, bold)
|
|
58
64
|
if (ctx.measureText(text).width <= maxW) return
|
|
59
65
|
}
|
|
60
66
|
}
|
package/src/assets/run-assets.ts
CHANGED
|
@@ -50,7 +50,7 @@ async function rollOnce(root: string, io: AssetsIo, kit: ResolvedKit, resolveCom
|
|
|
50
50
|
for (let i = 0; i < count; i++) {
|
|
51
51
|
const seed = randomSeed()
|
|
52
52
|
try {
|
|
53
|
-
await generateRoll(f!, { prompt, seed, destDir: dir, context }, resolveComfy)
|
|
53
|
+
await generateRoll(f!, { prompt, seed, destDir: dir, context, rollFormat: kit.rollFormat }, resolveComfy)
|
|
54
54
|
io.print(`ok: ${assetId} v${seed}`)
|
|
55
55
|
} catch (e) {
|
|
56
56
|
failed++
|
|
@@ -196,7 +196,7 @@ export const runAssetsCheck = (root: string, io: AssetsIo): Promise<number> =>
|
|
|
196
196
|
const gen = kind.generate
|
|
197
197
|
if (gen.backend !== 'comfy') continue
|
|
198
198
|
try {
|
|
199
|
-
const lw = workflowFor(wfs, { workflow: gen.workflow, assetId: kind.assets[0]?.id })
|
|
199
|
+
const lw = workflowFor(wfs, { workflow: gen.workflow, kind: kind.id, assetId: kind.assets[0]?.id })
|
|
200
200
|
if (gen.vars != null) {
|
|
201
201
|
for (const [, d] of lw.wf.entries()) d.reset()
|
|
202
202
|
applyVarValues(lw.wf, gen.vars, `kind '${kind.id}'`)
|
|
@@ -215,7 +215,7 @@ export const runAssetsCheck = (root: string, io: AssetsIo): Promise<number> =>
|
|
|
215
215
|
for (const f of allAssets(kit).filter((x) => x.kind.generate.backend === 'comfy')) {
|
|
216
216
|
let lw: LoadedWorkflow
|
|
217
217
|
try {
|
|
218
|
-
lw = workflowFor(wfs, { workflow: (f.kind.generate as ComfyGen).workflow, assetId: f.asset.id })
|
|
218
|
+
lw = workflowFor(wfs, { workflow: (f.kind.generate as ComfyGen).workflow, kind: f.kind.id, assetId: f.asset.id })
|
|
219
219
|
} catch {
|
|
220
220
|
continue // an unresolvable workflow already FAILed on its kind above
|
|
221
221
|
}
|
|
@@ -110,7 +110,7 @@ export async function studioRoutes(deps: StudioDeps): Promise<StudioRoutes> {
|
|
|
110
110
|
for (const f of allAssets(kit).filter((x) => x.kind.generate.backend === 'comfy')) {
|
|
111
111
|
try {
|
|
112
112
|
const gen = f.kind.generate as { workflow?: string }
|
|
113
|
-
const lw = workflowFor(workflows, { workflow: gen.workflow, assetId: f.asset.id })
|
|
113
|
+
const lw = workflowFor(workflows, { workflow: gen.workflow, kind: f.kind.id, assetId: f.asset.id })
|
|
114
114
|
// the KIND's draft, shared by every asset of that kind
|
|
115
115
|
if (ensureAssetDraft(lw, f.kind.id, await promptOf(f))) console.log(`ok: draft '${f.kind.id}' seeded → .comfy-ts/drafts/${lw.id}/${f.kind.id}.json`)
|
|
116
116
|
} catch (e) {
|
|
@@ -127,7 +127,7 @@ export async function studioRoutes(deps: StudioDeps): Promise<StudioRoutes> {
|
|
|
127
127
|
return { workflows, heal: kit.comfy.heal }
|
|
128
128
|
}
|
|
129
129
|
const generateOne = deps.generateOne ?? ((found, prompt, seed, destDir, onProgress, context?: ParentContext) =>
|
|
130
|
-
generateRoll(found, { prompt, seed, destDir, onProgress, context }, resolveComfy))
|
|
130
|
+
generateRoll(found, { prompt, seed, destDir, onProgress, context, rollFormat: kit.rollFormat }, resolveComfy))
|
|
131
131
|
|
|
132
132
|
const queue = new StudioQueue(async (job, seed, onProgress) => {
|
|
133
133
|
const found = findAsset(kit, job.assetId)
|
|
@@ -388,7 +388,7 @@ export async function studioRoutes(deps: StudioDeps): Promise<StudioRoutes> {
|
|
|
388
388
|
const rgen = running?.kind.generate
|
|
389
389
|
if (workflows != null && rgen?.backend === 'comfy') {
|
|
390
390
|
try {
|
|
391
|
-
await workflowFor(workflows, { workflow: rgen.workflow, assetId: running!.asset.id }).wf.host.interrupt()
|
|
391
|
+
await workflowFor(workflows, { workflow: rgen.workflow, kind: running!.kind.id, assetId: running!.asset.id }).wf.host.interrupt()
|
|
392
392
|
} catch (e) {
|
|
393
393
|
queue.pushError(`interrupt: ${(e as Error).message} — current roll will finish, its job is still dropped`)
|
|
394
394
|
}
|
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)) {
|