rvlib-shipkit 0.30.0 → 0.32.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 +14 -0
- package/README.md +3 -0
- package/package.json +1 -1
- package/src/cli/cmd_Bump.ts +2 -0
- package/src/cli/cmd_Ci.ts +3 -1
- package/src/cli/cmd_Pb.ts +27 -0
- package/src/cli/cmd_Release.ts +1 -1
- package/src/cli/cmd_Wiki.ts +2 -2
- package/src/cli.ts +49 -39
- package/src/collections/graphs.ts +17 -19
- package/src/collections/providers.ts +7 -2
- package/src/contracts/contracts-once.ts +43 -0
- package/src/core/canonical-scripts.ts +33 -0
- package/src/core/check-ctx.ts +13 -0
- package/src/core/check-suite.ts +121 -53
- package/src/core/floor.ts +82 -0
- package/src/core/motherhome.ts +14 -1
- package/src/core/run-check.ts +34 -0
- package/src/core/run-ci.ts +63 -2
- package/src/core/run-fixers.ts +6 -1
- package/src/ledger/toolchain-identity.ts +134 -0
- package/src/pb/collection-scaffold.ts +299 -0
- package/src/pb/pb-types.ts +233 -0
- package/src/pb/run-pb.ts +157 -0
- package/src/release/git-facts.ts +3 -2
- package/src/release/release-scope.ts +52 -0
- package/src/release/releases-share.ts +85 -0
- package/src/release/run-bump.ts +31 -8
- package/src/release/run-release.ts +8 -1
- package/src/release/run-releases.ts +20 -6
- package/src/repo-config.ts +22 -65
- package/src/rules/SK001-package-json-shape.ts +65 -2
- package/src/rules/SK002-contracts-fulfillness.ts +2 -2
- package/src/rules/SK004-gitignore-policy.ts +27 -3
- package/src/rules/SK012-config-barrel-import.ts +60 -0
- package/src/rules/rule-registry.ts +2 -0
- package/src/rules/untrack-generated.ts +117 -0
- package/src/scaffold/project-scaffold.ts +1 -1
- package/src/serve/hub-port.ts +17 -0
- package/src/serve/run-serve.ts +54 -20
- package/src/serve/serve-hub.ts +13 -15
- package/src/shipkit.ts +3 -2
- package/src/wiki/graph-mount.ts +21 -0
- package/src/wiki/run-wiki.ts +18 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.32.0
|
|
4
|
+
|
|
5
|
+
- **`shipkit pb collection <name>` scaffolds a PocketBase collection's whole UI folder.** Building a CRUD screen meant retyping a schema that already exists in the app's generated types, one column at a time, and the boring half of that is exactly where a column goes missing. The command reads `src/pb.types.ts` (`.tsx` and `.d.ts` too), resolves one collection, and writes `src/collections/<name>/`: `columns.tsx` with one entry per response field, `card.tsx`, `grid.tsx` over `LiveGrid`, `form.tsx` (a create/edit modal writing through `db` fire and forget), and an `index.ts` calling `defineCollectionUI`. It then registers the folder in `src/collections/index.ts`, creating that file when it is the first one, because a collection folder nothing imports is dead code whose failure mode is a runtime throw. A field the schema cannot turn into a cell on its own (relation, file, json, raw html) is emitted COMMENTED with the reason rather than dropped, because a missing column is invisible and a commented one takes ten seconds to accept or delete. The card leads on the fields a human reads, not on the first three of an alphabetical record block, and the label picks the first of `name`, `label`, `title`, `number`, `code`, `email`, else `id`.
|
|
6
|
+
- **The output is the house dialect, not a second one.** `X.` Mantine globals rather than `@mantine/core` imports, `observer(function Name_())` around the form, the `{ record?, onClose }` props `CollectionUI['form']` actually declares, and `defineCollectionUI` called as the void statement it is. A generator that invents its own shape is worse than no generator: what it writes is what the next developer copies. `key` is set only where the server can really order by the column, so a joined multi-value cell no longer claims sortability it does not have, and an auth collection's `verified`, `email`, `username` and `emailVisibility` get no control, since PocketBase routes those through endpoints a plain patch never reaches.
|
|
7
|
+
- **It refuses rather than half-writing.** No generated types, a file that is not typegen output, a collection the schema does not carry, one of PocketBase's own `_`-prefixed internals, a record block that is missing or unreadable, or a block where not one field parsed: each exits non-zero naming the file, the known collection names and the typegen command that produces them. A single field whose type is unrecognised is named in a `WARN` instead of quietly becoming a commented column. Writes are create-only: a path that already exists is reported and left byte for byte as it is, so a re-run after a schema change can never eat hand-written UI. `--dry-run` prints the plan AND every file body in full and touches nothing, which is what an agent should reach for first.
|
|
8
|
+
- **A path shipkit generates is ignored AND untracked, in one operation.** `.gitignore` does nothing to a file that is already tracked, so a generated path that is only ignored keeps dirtying every tree forever and rides into unrelated commits as a mystery diff. Rule SK004 now FAILs while a declared generated path is still in the index, and its autofix removes it with `git rm --cached` and commits exactly that, `chore(machine): untrack <path>`, through the hooks. The file stays on disk. First customers: `STATUS.md`, `STATUS.md.lock` and `.STATUS.md.tmp-*`, which shipkit rewrites on every run and nothing ever reads a past version of. Only literal paths are removable, never a glob, because `git rm --cached` driven by a wildcard is how a fixer eats source. It refuses, by name, mid-merge, mid-rebase, on a detached HEAD, before the first commit, and whenever anything else is already staged, so the migration commit can never sweep up someone's work. A hook that rejects it restores the index rather than leaving a staged deletion behind, which is the failure mode it exists to stop. `--frozen` suppresses it and `untrackGenerated: false` opts a repo out.
|
|
9
|
+
- **Two test tiers, and every run says which one it is.** `shipkit ci` runs the QUICK tier: pure logic, no child process, no repository on disk, so the working gate stays fast. `shipkit bump` and `shipkit release` always run the FULL tier, before the version commit, so a red suite aborts the release instead of leaving a half-published repo. `shipkit ci --full` (or `SHIPKIT_TEST_FULL=1`, which the gate sets for every task) runs it by hand. Both directions print a tier line and the test count actually run, because a log that does not name the tier cannot be told from one that quietly skipped half the suite.
|
|
10
|
+
- Reading the schema is a LINE parse of generated code, not a TypeScript AST: shipkit carries no typescript dependency and typegen writes one field per line. Record blocks parameterised by a json field are handled, and so is a file a formatter has rewritten with trailing semicolons, which is the normal state of any repo whose formatter covers `src/**`.
|
|
11
|
+
|
|
12
|
+
## 0.31.0
|
|
13
|
+
|
|
14
|
+
- **Every repo browses its shared wiki from its own hub.** A repo whose CLAUDE.md include chain points at a wiki living in another repo (`@~/dev/<other-repo>/wiki/index.md`, read from `CLAUDE.md`, `CLAUDE.local.md` or `CLAUDE.private.md`) mounts it at `/graph/base`, named after the repo that owns it. That wiki binds every agent working here, so the hub lets a human read it where the work happens. An index inside the repo is skipped, since it is already its own mount, and the card carries no lint verdict: the owning repo lints its own pages.
|
|
15
|
+
- **The graphs section stops advertising a graph the server does not serve.** A repo without a `wiki/` dir, which every fresh `shipkit start` is, still got a `wiki graph` card, and opening it embedded the hub's own 404 body ("not found — the hub page is at /") inside the card. Routes, `/hub.json` tools and cards now read ONE `GraphMount[]` that `resolveHubDeps` resolves (`HubDeps.wikiGraph` and `HubDeps.agentGraph` are gone, `HubDeps.graphs` replaces both), instead of the collection re-deriving the wiki graph from the repo root. A graph that is not mounted has no card, and the test asserts every href the section advertises answers 200.
|
|
16
|
+
|
|
3
17
|
## 0.30.0
|
|
4
18
|
|
|
5
19
|
- **The studio asset page is tools on one shared canvas, not tabs on a drawer.** Every metadata family is a toggleable TOOL: `oval`, `legs`, `size` (the geometry editor split into its parts), each repo `customPanel`, `asset json`, `roll meta`. Several can be on at once, the URL records the set (`#<assetId>?tools=a,b`, shareable, restored on load), and the last set per kind is remembered. Kit-declared TABS (`meta.tabs: [{ id, tools }]`) are one-click presets, validated at kit load (an undeclared panel OR an unknown tool id refuses, loud). Panel tools float as draggable cards ON the canvas (a drag clamps inside the stage), `customPanel({ anchor: 'bl' })` starts a card bottom-left, so switching a tab never reflows the stage, and the drawer, its tab row and the bottom json body are deleted. Toggling a custom editor on does fold the variants bar, the one deliberate reflow. Custom panels can draw on the art in place: `api.stageOverlay()` returns an element covering the displayed roll (the tint-mask painter paints THERE, controls stay in its floating card), `api.dropOverlays()` cleans up on unmount, and the host calls it. Variants (rolls) move to a thin resizable right bar (drag handle, width remembered, slim-rail collapse, auto-folds when a custom editor turns on). The prompt keeps its fold. The stage remeasures through a ResizeObserver, so overlays track the image through any layout change, not only window resizes.
|
package/README.md
CHANGED
|
@@ -46,6 +46,9 @@ shipkit wiki serve|lint — live md link-graph + orphan/dangling-ref lint
|
|
|
46
46
|
shipkit mv rename an md page + its sibling dir, sweeping every ref
|
|
47
47
|
shipkit assets serve|fill|add|gen|identity|check — the asset atelier
|
|
48
48
|
(declare kinds in .rv-assets/kit.ts, roll, curate, ship)
|
|
49
|
+
shipkit pb collection <name> — scaffold a PocketBase collection's UI
|
|
50
|
+
folder (columns/card/grid/form/index) from the app's
|
|
51
|
+
generated pb types, create-only [--dry-run]
|
|
49
52
|
shipkit serve the repo's ONE server: hub page, graphs, studio, status
|
|
50
53
|
shipkit reflect your prompts across every Claude Code session of a repo:
|
|
51
54
|
all config roots, queued-mid-run ones included, harness
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rvlib-shipkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"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",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/cli/cmd_Bump.ts
CHANGED
|
@@ -32,6 +32,7 @@ export class BumpCommand extends Command {
|
|
|
32
32
|
dryRun = Option.Boolean('--dry-run', false, { description: 'rehearse: run every preflight, the gate and the prepublish scripts, then stop before any version is written. the gate can rewrite files, and the merge is probed rather than performed, so the release branch never moves' })
|
|
33
33
|
noRelease = Option.Boolean('--no-release', false, { description: 'stop after the version commit, do not tag or publish' })
|
|
34
34
|
noMerge = Option.Boolean('--no-merge', false, { description: 'refuse on a feature branch instead of merging it into the release branch' })
|
|
35
|
+
allowDirty = Option.Boolean('--allow-dirty', false, { description: "proceed with uncommitted changes inside what this release writes. Dirt in OTHER units is already ignored, so this is only for when the scoping judges wrong. A sub-unit's OWN directory still refuses, the same as `release`, and no flag lifts that: `npm publish` packs the WORKING TREE rather than the commit, so forcing past it ships the half-written file under a tag that describes something else" })
|
|
35
36
|
all = Option.Boolean('--all', false, { description: 'bump every unit whose changelog carries Unreleased notes, in declared order, root last. Given a kind it applies to ALL of them. Given none, each unit takes the kind from its own "## Unreleased (minor)" heading, and a unit whose heading is silent refuses' })
|
|
36
37
|
|
|
37
38
|
async execute(): Promise<number> {
|
|
@@ -51,6 +52,7 @@ export class BumpCommand extends Command {
|
|
|
51
52
|
noMerge: this.noMerge,
|
|
52
53
|
unit,
|
|
53
54
|
all: this.all,
|
|
55
|
+
allowDirty: this.allowDirty,
|
|
54
56
|
print: (l) => this.context.stdout.write(`${l}\n`),
|
|
55
57
|
})
|
|
56
58
|
} catch (e) {
|
package/src/cli/cmd_Ci.ts
CHANGED
|
@@ -24,10 +24,11 @@ export class CiCommand extends Command {
|
|
|
24
24
|
json = Option.Boolean('--json', false, { description: 'machine-readable result instead of wire lines' })
|
|
25
25
|
fix = Option.Boolean('--fix', true, { description: 'repair the mechanical slots before gating. Default on. --no-fix skips the repair pass, checks can still write. Under CI the fixers still run but the gate FAILS on any file they rewrite' })
|
|
26
26
|
fixRules = Option.Boolean('--fix-rules', true, { description: 'apply the structural rule autofixes too. Default on. --no-fix-rules leaves repo config alone' })
|
|
27
|
-
frozen = Option.Boolean('--frozen', false, { description: 'report-only mode for self-maintaining rules
|
|
27
|
+
frozen = Option.Boolean('--frozen', false, { description: 'report-only mode for self-maintaining rules: SK005 never touches package.json, and generated paths are never untracked. STATUS.md is still refreshed on disk, but it is gitignored, so a run leaves the tree clean' })
|
|
28
28
|
skip = Option.Array('--skip', [], { description: 'skip a task group (repeatable)' })
|
|
29
29
|
withIds = Option.Array('--with', [], { description: 'include an opt-in task (repeatable)' })
|
|
30
30
|
only = Option.Array('--only', [], { description: 'run only this task + its dep closure (repeatable)' })
|
|
31
|
+
full = Option.Boolean('--full', false, { description: `run the FULL test tier: every suite the repo declares, the slow ones it skips while you work included. What the release path always runs. Same as SHIPKIT_TEST_FULL=1` })
|
|
31
32
|
|
|
32
33
|
async execute(): Promise<number> {
|
|
33
34
|
// a stale toolkit updates itself and restarts this exact invocation
|
|
@@ -44,6 +45,7 @@ export class CiCommand extends Command {
|
|
|
44
45
|
skipGroups: this.skip,
|
|
45
46
|
withIds: this.withIds,
|
|
46
47
|
onlyIds: this.only,
|
|
48
|
+
...(this.full ? { tests: 'full' as const } : {}),
|
|
47
49
|
print: (l) => this.context.stdout.write(`${l}\n`),
|
|
48
50
|
})
|
|
49
51
|
} catch (e) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// shipkit pb — scaffolding for a PocketBase app, run from THAT app's root.
|
|
2
|
+
// Thin clipanion surface; the schema parse and the emitters live in pb/.
|
|
3
|
+
import { Command, Option } from 'clipanion'
|
|
4
|
+
import { runPbCollection } from '../pb/run-pb.ts'
|
|
5
|
+
|
|
6
|
+
export class PbCollectionCommand extends Command {
|
|
7
|
+
static override paths = [['pb', 'collection']]
|
|
8
|
+
|
|
9
|
+
static override usage = Command.Usage({
|
|
10
|
+
category: 'Scaffold',
|
|
11
|
+
description: "a collection's CRUD UI folder from the app's generated pb types: columns, card, grid, form, index",
|
|
12
|
+
examples: [
|
|
13
|
+
['see the whole folder before it exists', 'shipkit pb collection orders --dry-run'],
|
|
14
|
+
['write src/collections/orders/', 'shipkit pb collection orders'],
|
|
15
|
+
],
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
name = Option.String()
|
|
19
|
+
dryRun = Option.Boolean('--dry-run', false, { description: 'print every file in full, write nothing' })
|
|
20
|
+
|
|
21
|
+
async execute(): Promise<number> {
|
|
22
|
+
return runPbCollection(process.cwd(), this.name, {
|
|
23
|
+
print: (l) => this.context.stdout.write(`${l}\n`),
|
|
24
|
+
printErr: (l) => this.context.stderr.write(`${l}\n`),
|
|
25
|
+
}, { dryRun: this.dryRun })
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/cli/cmd_Release.ts
CHANGED
|
@@ -20,7 +20,7 @@ export class ReleaseCommand extends Command {
|
|
|
20
20
|
|
|
21
21
|
unit = Option.String({ required: false })
|
|
22
22
|
dryRun = Option.Boolean('--dry-run', false, { description: 'run every preflight and print the plan, change nothing' })
|
|
23
|
-
allowDirty = Option.Boolean('--allow-dirty', false, { description: 'release from an unclean tree (the tag will not contain it).
|
|
23
|
+
allowDirty = Option.Boolean('--allow-dirty', false, { description: 'release from an unclean tree (the tag will not contain it). Safe for dirt in OTHER paths only: `npm publish` packs the WORKING TREE, not the commit, so a SUB-unit still refuses on dirt inside its own path rather than ship a half-written file under a tag that describes something else' })
|
|
24
24
|
skipGate = Option.Boolean('--skip-gate', false, { description: 'skip the ci gate preflight — publishes unverified, use only to recover a stuck release' })
|
|
25
25
|
|
|
26
26
|
async execute(): Promise<number> {
|
package/src/cli/cmd_Wiki.ts
CHANGED
|
@@ -27,7 +27,7 @@ export class WikiServeCommand extends Command {
|
|
|
27
27
|
this.context.stderr.write(`FAIL --port '${this.port}' is not a port number\n`)
|
|
28
28
|
return 1
|
|
29
29
|
}
|
|
30
|
-
return runWikiServe({ root: this.root, index: this.index, skip: this.skip, port, title: this.title })
|
|
30
|
+
return runWikiServe({ root: this.root, index: this.index, skip: this.skip, port, title: this.title, print: (l) => this.context.stdout.write(`${l}\n`) })
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -48,6 +48,6 @@ export class WikiLintCommand extends Command {
|
|
|
48
48
|
skip = Option.String('--skip', { required: false, description: 'comma-separated dir names to exclude from the scan' })
|
|
49
49
|
|
|
50
50
|
async execute(): Promise<number> {
|
|
51
|
-
return runWikiLint({ root: this.root, index: this.index, skip: this.skip })
|
|
51
|
+
return runWikiLint({ root: this.root, index: this.index, skip: this.skip, print: (l) => this.context.stdout.write(`${l}\n`) })
|
|
52
52
|
}
|
|
53
53
|
}
|
package/src/cli.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { HooksCommitMsgCommand, HooksInstallCommand, HooksListCommand, HooksPreC
|
|
|
15
15
|
import { InitCommand } from './cli/cmd_Init.ts'
|
|
16
16
|
import { MvCommand } from './cli/cmd_Mv.ts'
|
|
17
17
|
import { NeedsCommand } from './cli/cmd_Needs.ts'
|
|
18
|
+
import { PbCollectionCommand } from './cli/cmd_Pb.ts'
|
|
18
19
|
import { ReflectCommand } from './cli/cmd_Reflect.ts'
|
|
19
20
|
import { ReleaseCommand } from './cli/cmd_Release.ts'
|
|
20
21
|
import { ReleasesCommand } from './cli/cmd_Releases.ts'
|
|
@@ -26,44 +27,53 @@ import { WikiLintCommand, WikiServeCommand } from './cli/cmd_Wiki.ts'
|
|
|
26
27
|
|
|
27
28
|
const version = (JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as { version: string }).version
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
/** the registry, built but NOT run: a test drives the real binary in process
|
|
31
|
+
* instead of paying a cold bun start per assertion (~250ms each), and the
|
|
32
|
+
* wiring it exercises is the wiring that ships. */
|
|
33
|
+
export function buildCli(): Cli {
|
|
34
|
+
const cli = new Cli({
|
|
35
|
+
binaryLabel: 'shipkit — repo toolkit',
|
|
36
|
+
binaryName: 'shipkit',
|
|
37
|
+
binaryVersion: version,
|
|
38
|
+
enableCapture: true, // console output pipes into the command streams (testable)
|
|
39
|
+
enableColors: true,
|
|
40
|
+
})
|
|
36
41
|
|
|
37
|
-
cli.register(Builtins.HelpCommand)
|
|
38
|
-
cli.register(Builtins.VersionCommand)
|
|
39
|
-
cli.register(CiCommand)
|
|
40
|
-
cli.register(BumpCommand)
|
|
41
|
-
cli.register(ReleaseCommand)
|
|
42
|
-
cli.register(ReleasesCommand)
|
|
43
|
-
cli.register(HooksInstallCommand)
|
|
44
|
-
cli.register(HooksPreCommitCommand)
|
|
45
|
-
cli.register(HooksCommitMsgCommand)
|
|
46
|
-
cli.register(HooksListCommand)
|
|
47
|
-
cli.register(MvCommand)
|
|
48
|
-
cli.register(CheckCommand)
|
|
49
|
-
cli.register(TuiCommand)
|
|
50
|
-
cli.register(ServeCommand)
|
|
51
|
-
cli.register(ContractsCommand)
|
|
52
|
-
cli.register(AsksCommand)
|
|
53
|
-
cli.register(AsksCheckCommand)
|
|
54
|
-
cli.register(AskNewCommand)
|
|
55
|
-
cli.register(WikiServeCommand)
|
|
56
|
-
cli.register(WikiLintCommand)
|
|
57
|
-
cli.register(AssetsFillCommand)
|
|
58
|
-
cli.register(AssetsAddCommand)
|
|
59
|
-
cli.register(AssetsGenCommand)
|
|
60
|
-
cli.register(AssetsIdentityCommand)
|
|
61
|
-
cli.register(AssetsCheckCommand)
|
|
62
|
-
cli.register(InitCommand)
|
|
63
|
-
cli.register(StartCommand)
|
|
64
|
-
cli.register(NeedsCommand)
|
|
65
|
-
cli.register(
|
|
66
|
-
cli.register(
|
|
67
|
-
cli.register(
|
|
42
|
+
cli.register(Builtins.HelpCommand)
|
|
43
|
+
cli.register(Builtins.VersionCommand)
|
|
44
|
+
cli.register(CiCommand)
|
|
45
|
+
cli.register(BumpCommand)
|
|
46
|
+
cli.register(ReleaseCommand)
|
|
47
|
+
cli.register(ReleasesCommand)
|
|
48
|
+
cli.register(HooksInstallCommand)
|
|
49
|
+
cli.register(HooksPreCommitCommand)
|
|
50
|
+
cli.register(HooksCommitMsgCommand)
|
|
51
|
+
cli.register(HooksListCommand)
|
|
52
|
+
cli.register(MvCommand)
|
|
53
|
+
cli.register(CheckCommand)
|
|
54
|
+
cli.register(TuiCommand)
|
|
55
|
+
cli.register(ServeCommand)
|
|
56
|
+
cli.register(ContractsCommand)
|
|
57
|
+
cli.register(AsksCommand)
|
|
58
|
+
cli.register(AsksCheckCommand)
|
|
59
|
+
cli.register(AskNewCommand)
|
|
60
|
+
cli.register(WikiServeCommand)
|
|
61
|
+
cli.register(WikiLintCommand)
|
|
62
|
+
cli.register(AssetsFillCommand)
|
|
63
|
+
cli.register(AssetsAddCommand)
|
|
64
|
+
cli.register(AssetsGenCommand)
|
|
65
|
+
cli.register(AssetsIdentityCommand)
|
|
66
|
+
cli.register(AssetsCheckCommand)
|
|
67
|
+
cli.register(InitCommand)
|
|
68
|
+
cli.register(StartCommand)
|
|
69
|
+
cli.register(NeedsCommand)
|
|
70
|
+
cli.register(PbCollectionCommand)
|
|
71
|
+
cli.register(ReflectCommand)
|
|
72
|
+
cli.register(RulesCommand)
|
|
73
|
+
cli.register(CommandsCommand)
|
|
68
74
|
|
|
69
|
-
cli
|
|
75
|
+
return cli
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// only when this file IS the process, so importing it costs nothing
|
|
79
|
+
if (import.meta.main) void buildCli().runExit(process.argv.slice(2))
|
|
@@ -1,34 +1,32 @@
|
|
|
1
|
-
// graphs collection — the
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
import { join } from 'path'
|
|
1
|
+
// graphs collection — the MOUNTED md graphs as CHECKED section entries. The
|
|
2
|
+
// mounts arrive as data from the hub's own route resolution (serve/run-serve),
|
|
3
|
+
// never re-derived here: a card linking at an unmounted route renders the 404
|
|
4
|
+
// body inside its iframe. Each instance carries its lint state (orphans +
|
|
5
|
+
// dangling refs = yellow with counts; clean = quiet). A graph whose lint
|
|
6
|
+
// belongs to another repo is link-only, never double-judged here.
|
|
8
7
|
import { lintMd } from '../wiki/lint-md.ts'
|
|
8
|
+
import type { GraphMount } from '../wiki/graph-mount.ts'
|
|
9
9
|
import type { Collection, Instance } from './collection-types.ts'
|
|
10
10
|
|
|
11
|
-
export function graphsCollection(
|
|
11
|
+
export function graphsCollection(mounts: GraphMount[]): Collection {
|
|
12
12
|
return {
|
|
13
13
|
id: 'graphs',
|
|
14
14
|
title: 'graphs',
|
|
15
15
|
media: 'data',
|
|
16
|
-
list: async () =>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
16
|
+
list: async () =>
|
|
17
|
+
mounts.map((m): Instance => {
|
|
18
|
+
const id = `graph:${m.id}`
|
|
19
|
+
const href = `/graph/${m.id}`
|
|
20
|
+
const label = m.note != null ? `${m.label} (${m.note})` : m.label
|
|
21
|
+
if (m.lint == null) return { id, text: label, href }
|
|
22
|
+
const r = lintMd({ root: m.lint.root, index: m.lint.index })
|
|
20
23
|
const issues = r.orphans.length + r.dangling.length
|
|
21
24
|
return {
|
|
22
25
|
id,
|
|
23
|
-
text: issues === 0 ?
|
|
26
|
+
text: issues === 0 ? label : `${label} — ${r.orphans.length} orphan(s), ${r.dangling.length} dangling ref(s)`,
|
|
24
27
|
severity: issues === 0 ? undefined : 'yellow',
|
|
25
28
|
href,
|
|
26
29
|
}
|
|
27
|
-
}
|
|
28
|
-
if (o.customWiki) out.push({ id: 'graph:wiki', text: 'wiki graph (repo-owned lint)', href: '/graph/wiki' })
|
|
29
|
-
else out.push(checked('graph:wiki', 'wiki graph', '/graph/wiki', root))
|
|
30
|
-
if (existsSync(join(root, 'agent'))) out.push(checked('graph:agent', 'agent graph', '/graph/agent', join(root, 'agent'), o.agentIndex))
|
|
31
|
-
return out
|
|
32
|
-
},
|
|
30
|
+
}),
|
|
33
31
|
}
|
|
34
32
|
}
|
|
@@ -13,7 +13,10 @@ import { join } from 'path'
|
|
|
13
13
|
import { isStale } from '../assets/gen/derived.ts'
|
|
14
14
|
import { allAssets, mediaOf, type ResolvedKit } from '../assets/kit-config.ts'
|
|
15
15
|
import { assetDir, listRolls, readPick } from '../assets/roll-workspace.ts'
|
|
16
|
-
import
|
|
16
|
+
// TYPE only, plus a lazy import at the call site. run-assets pulls the comfy
|
|
17
|
+
// runtime and the canvas binding, ~400ms of module load, and the hub's
|
|
18
|
+
// collection list is built on every request by callers that mostly never heal
|
|
19
|
+
import type { healIdentity } from '../assets/run-assets.ts'
|
|
17
20
|
import { doneAsks, openAsks } from '../asks/asks-queue.ts'
|
|
18
21
|
import { evaluateContracts } from '../contracts/contracts-engine.ts'
|
|
19
22
|
import { parseWireOutput } from '../core/wire.ts'
|
|
@@ -171,7 +174,9 @@ const credInstance = (name: string, cmdTemplate: string | undefined, decl?: { se
|
|
|
171
174
|
* Identity self-heals BEFORE surfacing; manual with nothing uploaded =
|
|
172
175
|
* red (provide); rolls with no pick = yellow (a default exists, taste
|
|
173
176
|
* pending — auto-pick-first is blessed); stale = yellow. */
|
|
174
|
-
|
|
177
|
+
const healLazily: typeof healIdentity = async (root) => (await import('../assets/run-assets.ts')).healIdentity(root)
|
|
178
|
+
|
|
179
|
+
export function mediaKitCollection(root: string, kit: ResolvedKit | null, heal: typeof healIdentity = healLazily): Collection {
|
|
175
180
|
return {
|
|
176
181
|
id: 'media',
|
|
177
182
|
title: 'assets',
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// ONE contract evaluation per gate run.
|
|
2
|
+
//
|
|
3
|
+
// Two surfaces report fulfillness inside a single `shipkit ci`: the `contracts`
|
|
4
|
+
// TASK, which exists because deploy tasks declare `deps: ['contracts']` and so
|
|
5
|
+
// need a real ordering edge in the DAG, and rule SK002, which exists because a
|
|
6
|
+
// `shipkit check` with no task phase must still report it. Both are load
|
|
7
|
+
// bearing and neither can be deleted: dropping the task would silently ungate
|
|
8
|
+
// every deploy that depends on it, since checks run AFTER every task.
|
|
9
|
+
//
|
|
10
|
+
// So they stay two REPORTS of one evaluation. The task runs first and its
|
|
11
|
+
// result is what SK002 reads. THE TASK IS AUTHORITATIVE (contracts.md says so
|
|
12
|
+
// too); SK002 evaluating on its own is the fallback for a run with no task
|
|
13
|
+
// phase, not a second opinion.
|
|
14
|
+
//
|
|
15
|
+
// The scope is EXPLICIT and never ambient. A memo that outlived its run would
|
|
16
|
+
// let the hub, which rebuilds its collection list on every request, answer
|
|
17
|
+
// from a snapshot of whatever the last gate saw. Only a caller that owns a run
|
|
18
|
+
// opens one.
|
|
19
|
+
import { evaluateContracts, type ContractsConfig, type ContractsResult } from './contracts-engine.ts'
|
|
20
|
+
import type { ContractManifest } from './contract-types.ts'
|
|
21
|
+
|
|
22
|
+
let open = false
|
|
23
|
+
let cached: { key: string; result: Promise<ContractsResult> } | null = null
|
|
24
|
+
|
|
25
|
+
/** run `fn` as ONE gate run: contract evaluation inside it happens once. */
|
|
26
|
+
export async function withContractsScope<T>(fn: () => Promise<T>): Promise<T> {
|
|
27
|
+
const outer = open
|
|
28
|
+
open = true
|
|
29
|
+
try { return await fn() } finally { if (!outer) { open = false; cached = null } }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** the evaluation, memoised only while a run scope is open. Outside one it is
|
|
33
|
+
* a plain call, so `shipkit contracts` and the hub always measure reality. */
|
|
34
|
+
export function evaluateContractsOnce(manifests: ContractManifest[], cfg: ContractsConfig): Promise<ContractsResult> {
|
|
35
|
+
if (!open) return evaluateContracts(manifests, cfg)
|
|
36
|
+
// the config decides the verdict, so it belongs in the identity: a run that
|
|
37
|
+
// somehow evaluated two different configs must not serve one for the other
|
|
38
|
+
const key = JSON.stringify(cfg)
|
|
39
|
+
if (cached != null && cached.key === key) return cached.result
|
|
40
|
+
const result = evaluateContracts(manifests, cfg)
|
|
41
|
+
cached = { key, result }
|
|
42
|
+
return result
|
|
43
|
+
}
|
|
@@ -30,6 +30,39 @@ export function conditionalBodies(_opts: ScriptStyle = {}): Record<string, strin
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// ONE WAY IN (build/shipkit.md): the verification verbs an agent types from
|
|
34
|
+
// muscle memory ROUTE to shipkit, so `bun run test` gets the cache and the
|
|
35
|
+
// job attachment without anyone having to remember a rule. The tool body they
|
|
36
|
+
// used to hold moves to `<name>:raw`, which is what shipkit itself runs and
|
|
37
|
+
// what an agent uses to iterate on one repro file.
|
|
38
|
+
//
|
|
39
|
+
// only these three. `imports:check` and `cycles` are shipkit's own vocabulary
|
|
40
|
+
// (nobody reaches for them by reflex) and `fmt` is the slot shipkit's fix pass
|
|
41
|
+
// invokes, so routing it would make the fixer call itself.
|
|
42
|
+
export const ROUTED_SCRIPTS = ['test', 'lint', 'typecheck'] as const
|
|
43
|
+
|
|
44
|
+
export const RAW_SUFFIX = ':raw'
|
|
45
|
+
|
|
46
|
+
/** where the tool body lives once `<id>` routes to shipkit */
|
|
47
|
+
export const rawName = (id: string): string => `${id}${RAW_SUFFIX}`
|
|
48
|
+
|
|
49
|
+
/** what a routed `<id>` script says. `--only` narrows to the one question the
|
|
50
|
+
* name promises: `bun run test` must not become a full gate behind the
|
|
51
|
+
* agent's back */
|
|
52
|
+
export function routedBody(id: string, opts: ScriptStyle = {}): string {
|
|
53
|
+
return `${opts.bin != null ? `bun ${opts.bin}` : 'shipkit'} ci --only ${id}`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** does this body hand the question to `shipkit ci` rather than to a tool ?
|
|
57
|
+
* every invocation form counts: the bin, `bunx rvlib-shipkit`, an in-repo
|
|
58
|
+
* `…/cli.ts` path. A body that does is never run AS a floor task, on pain of
|
|
59
|
+
* infinite recursion. The `ci` half is load-bearing: a body that merely lives
|
|
60
|
+
* under a shipkit checkout (a repo calling rvlib's source-graph cli by path)
|
|
61
|
+
* is not a route back in. */
|
|
62
|
+
export function routesToShipkit(body: string): boolean {
|
|
63
|
+
return /\bshipkit\b/.test(body) && /\bci\b/.test(body)
|
|
64
|
+
}
|
|
65
|
+
|
|
33
66
|
// recover the consumption style from existing scripts: any shipkit command
|
|
34
67
|
// already running `bun <path>/cli.ts <cmd>` fixes the style for new bodies
|
|
35
68
|
export function inferBin(scripts: Record<string, string>): string | undefined {
|
package/src/core/check-ctx.ts
CHANGED
|
@@ -13,10 +13,23 @@ export class CheckCtx {
|
|
|
13
13
|
lines: Line[] = []
|
|
14
14
|
failed = false
|
|
15
15
|
warned = false
|
|
16
|
+
/** THIS RUN PRODUCED NO ANSWER: the probe could not run at all (network
|
|
17
|
+
* down, no cred, rate limited, tool missing). Distinct from a probe that
|
|
18
|
+
* ran and came back bad, which IS an answer. The cache replays answers
|
|
19
|
+
* and never replays their absence (check-suite.ts, the measured-negative
|
|
20
|
+
* rule). Set it whenever the reason a check is not green is that it never
|
|
21
|
+
* reached the thing it was asking about. */
|
|
22
|
+
unmeasured = false
|
|
16
23
|
constructor(private toolPaths: Map<string, string>) {}
|
|
17
24
|
|
|
18
25
|
ok(msg: string): void { this.lines.push({ kind: 'ok', text: msg }) }
|
|
19
26
|
note(msg: string): void { this.lines.push({ kind: 'note', text: msg }) }
|
|
27
|
+
/** a WARN that came back from something we could not reach — no answer, so
|
|
28
|
+
* nothing to remember */
|
|
29
|
+
unreachable(msg: string, fix?: string): void {
|
|
30
|
+
this.unmeasured = true
|
|
31
|
+
this.warn(msg, fix)
|
|
32
|
+
}
|
|
20
33
|
warn(msg: string, fix?: string): void {
|
|
21
34
|
this.warned = true
|
|
22
35
|
this.lines.push({ kind: 'warn', text: msg })
|