rvlib-shipkit 0.38.0 → 1.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 +371 -16
- package/README.md +26 -14
- package/package.json +2 -2
- package/src/assets/gen/comfyts-gen.ts +27 -10
- package/src/assets/gen/generate-roll.ts +1 -1
- package/src/assets/kit-config.ts +6 -6
- package/src/assets/run-assets.ts +14 -10
- package/src/assets/server/studio-server.ts +6 -5
- package/src/check.ts +343 -0
- package/src/{rules/SK001-package-json-shape.ts → checks/SK001-package-json-shape.check.ts} +44 -46
- package/src/checks/SK002-contracts-fulfillness.check.ts +24 -0
- package/src/checks/SK003-claude-md-shape.check.ts +66 -0
- package/src/checks/SK004-gitignore-policy.check.ts +309 -0
- package/src/{rules/SK005-rvlib-versions.ts → checks/SK005-rvlib-versions.check.ts} +46 -32
- package/src/{rules/SK006-npm-publish-safety.ts → checks/SK006-npm-publish-safety.check.ts} +25 -24
- package/src/{rules/SK007-github-visibility.ts → checks/SK007-github-visibility.check.ts} +20 -14
- package/src/{rules/SK008-reviewer-agent.ts → checks/SK008-reviewer-agent.check.ts} +29 -14
- package/src/{rules/SK009-commit-ai-tells.ts → checks/SK009-commit-ai-tells.check.ts} +24 -21
- package/src/checks/SK010-undeclared-unit.check.ts +111 -0
- package/src/{rules/SK011-sibling-unit-ranges.ts → checks/SK011-sibling-unit-ranges.check.ts} +21 -20
- package/src/{rules/SK012-config-barrel-import.ts → checks/SK012-config-barrel-import.check.ts} +18 -23
- package/src/{rules/SK013-one-way-in-policy.ts → checks/SK013-one-way-in-policy.check.ts} +58 -50
- package/src/{rules/SK014-prose-ste.ts → checks/SK014-prose-ste.check.ts} +27 -13
- package/src/{rules/SK015-nested-repo.ts → checks/SK015-nested-repo.check.ts} +15 -12
- package/src/checks/SK016-legacy-check-shapes.check.ts +182 -0
- package/src/checks/SK017-vscode-hub-task.check.ts +197 -0
- package/src/checks/SK018-test-files-run.check.ts +28 -0
- package/src/checks/SK019-tests-run-in-units.check.ts +76 -0
- package/src/checks/SK020-self-spawning-check.check.ts +147 -0
- package/src/checks/contracts.check.ts +20 -0
- package/src/cli/cmd_Asks.ts +3 -2
- package/src/cli/cmd_Assets.ts +2 -2
- package/src/cli/cmd_Bump.ts +1 -1
- package/src/cli/cmd_Check.ts +6 -6
- package/src/cli/cmd_Checks.ts +95 -0
- package/src/cli/cmd_Ci.ts +48 -13
- package/src/cli/cmd_Commands.ts +2 -6
- package/src/cli/cmd_ConfigKeys.ts +69 -0
- package/src/cli/cmd_Contracts.ts +1 -1
- package/src/cli/cmd_Fleet.ts +6 -2
- package/src/cli/cmd_Hooks.ts +1 -1
- package/src/cli/cmd_Keys.ts +43 -18
- package/src/cli/cmd_Mv.ts +2 -2
- package/src/cli/cmd_Prose.ts +10 -1
- package/src/cli/cmd_Releases.ts +2 -2
- package/src/cli/cmd_Serve.ts +1 -1
- package/src/cli/cmd_Start.ts +6 -6
- package/src/cli/cmd_Status.ts +25 -0
- package/src/cli/cmd_Tui.ts +2 -2
- package/src/cli.ts +7 -4
- package/src/collections/actions.ts +4 -4
- package/src/collections/checks.ts +258 -0
- package/src/collections/collection-types.ts +254 -1
- package/src/collections/comfy.ts +23 -0
- package/src/collections/md-body.ts +14 -0
- package/src/collections/pings.ts +38 -12
- package/src/collections/posts.ts +19 -3
- package/src/collections/providers.ts +198 -91
- package/src/collections/reflections.ts +41 -0
- package/src/collections/state-rules.ts +73 -0
- package/src/collections/test-time.ts +288 -0
- package/src/collections/tests.ts +181 -0
- package/src/collections/wikis.ts +66 -0
- package/src/comfy/comfy-panel.ts +292 -0
- package/src/contracts/apply-contracts.ts +17 -12
- package/src/contracts/contract-types.ts +16 -4
- package/src/contracts/contracts-engine.ts +78 -42
- package/src/contracts/manifests/discord.contract.ts +6 -6
- package/src/contracts/manifests/identity.contract.ts +3 -3
- package/src/contracts/manifests/itch.contract.ts +12 -15
- package/src/contracts/manifests/social.contract.ts +5 -5
- package/src/contracts/manifests/steam.contract.ts +16 -16
- package/src/contracts/run-contracts.ts +4 -3
- package/src/core/attach.ts +96 -0
- package/src/core/canonical-scripts.ts +25 -0
- package/src/core/check-ctx.ts +204 -14
- package/src/core/check-file-names.ts +112 -0
- package/src/core/check-pass.ts +84 -0
- package/src/core/check-runner.ts +796 -0
- package/src/core/config-keys.ts +256 -0
- package/src/core/config-rewrite.ts +77 -0
- package/src/core/discover-checks.ts +166 -90
- package/src/core/discover-legacy-checks.ts +26 -0
- package/src/core/doc-block.ts +13 -0
- package/src/core/floor-resolve.ts +264 -0
- package/src/core/floor.ts +20 -137
- package/src/core/git-bin.ts +24 -0
- package/src/core/git-root.ts +108 -0
- package/src/core/instance.ts +195 -0
- package/src/core/jobs.ts +118 -0
- package/src/core/layout.ts +52 -0
- package/src/core/legacy-shapes.ts +207 -0
- package/src/core/lib-root.ts +5 -0
- package/src/core/locked-write.ts +110 -39
- package/src/core/machine-commit.ts +5 -4
- package/src/core/migrate-layout.ts +557 -0
- package/src/core/motherhome.ts +2 -8
- package/src/core/nesting.ts +4 -12
- package/src/core/one-way-in-policy.ts +73 -17
- package/src/core/outcome-words.ts +45 -0
- package/src/core/package-manager.ts +86 -0
- package/src/core/parallel.ts +21 -0
- package/src/core/process-tree.ts +44 -0
- package/src/core/raw-access.ts +93 -0
- package/src/core/read-set.ts +152 -0
- package/src/core/run-check.ts +155 -213
- package/src/core/run-ci.ts +153 -189
- package/src/core/run-fix.ts +96 -0
- package/src/core/run-fixers.ts +53 -30
- package/src/core/self-update.ts +9 -3
- package/src/core/slots.ts +83 -0
- package/src/core/spawn-timeout.ts +34 -3
- package/src/core/when-block.ts +401 -0
- package/src/core/when.ts +135 -0
- package/src/core/write-status.ts +5 -2
- package/src/hooks/leak-guard.ts +10 -8
- package/src/hooks/one-way-in-guard.ts +13 -5
- package/src/hooks/root-identity.ts +2 -1
- package/src/hooks/run-hooks.ts +35 -23
- package/src/init-repo.ts +39 -23
- package/src/ledger/check-stats.ts +155 -0
- package/src/ledger/check-table.ts +179 -0
- package/src/ledger/db.ts +255 -0
- package/src/ledger/gate-checks.ts +433 -0
- package/src/ledger/git-key.ts +116 -77
- package/src/ledger/git-scan.ts +96 -35
- package/src/ledger/ledger.ts +116 -42
- package/src/ledger/live-runs.ts +111 -0
- package/src/ledger/live-timeline.ts +97 -0
- package/src/ledger/repo-view.ts +73 -0
- package/src/ledger/run-log.ts +448 -0
- package/src/ledger/run-timeline.ts +173 -0
- package/src/ledger/store.ts +23 -12
- package/src/ledger/suite-log.ts +161 -0
- package/src/ledger/toolchain-identity.ts +7 -1
- package/src/ledger/world-cache.ts +111 -0
- package/src/mv/mv-md.ts +16 -6
- package/src/problems/extract.ts +182 -0
- package/src/problems/problem-board.ts +37 -0
- package/src/problems/problem-line.ts +77 -0
- package/src/prose/prose-view.ts +90 -0
- package/src/prose/run-prose.ts +60 -19
- package/src/prose/ste.ts +140 -24
- package/src/reflections/reflections-queue.ts +81 -0
- package/src/release/git-facts.ts +15 -9
- package/src/release/run-bump.ts +6 -5
- package/src/release/run-release.ts +25 -11
- package/src/release/run-releases.ts +2 -2
- package/src/release/unit-gate.ts +7 -54
- package/src/release/unit-resolve.ts +282 -0
- package/src/release/units.ts +33 -217
- package/src/repo-config.ts +155 -85
- package/src/rules/rule.ts +36 -96
- package/src/rules/untrack-generated.ts +5 -4
- package/src/scaffold/files/vscode-tasks.json +10 -31
- package/src/scaffold/jsonc-spans.ts +113 -0
- package/src/scaffold/project-scaffold.ts +16 -43
- package/src/scan.ts +98 -0
- package/src/serve/gate-trigger.ts +148 -0
- package/src/serve/health-schedule.ts +102 -0
- package/src/serve/hub-app/chart-view.ts +179 -0
- package/src/serve/hub-app/check-view.tsx +345 -0
- package/src/serve/hub-app/check-words.ts +164 -0
- package/src/serve/hub-app/checks-svc.ts +280 -0
- package/src/serve/hub-app/clock-svc.ts +33 -0
- package/src/serve/hub-app/col-card.tsx +23 -39
- package/src/serve/hub-app/cols-svc.ts +44 -20
- package/src/serve/hub-app/comfy-svc.ts +171 -0
- package/src/serve/hub-app/comfy-view.tsx +81 -0
- package/src/serve/hub-app/dur.ts +41 -0
- package/src/serve/hub-app/hash-route.ts +121 -0
- package/src/serve/hub-app/header-bar.tsx +117 -0
- package/src/serve/hub-app/health-svc.ts +23 -28
- package/src/serve/hub-app/help.tsx +29 -0
- package/src/serve/hub-app/hub-app.tsx +122 -119
- package/src/serve/hub-app/hub-st.ts +115 -42
- package/src/serve/hub-app/hub-types.ts +117 -2
- package/src/serve/hub-app/hub.css +534 -192
- package/src/serve/hub-app/index.html +2 -0
- package/src/serve/hub-app/live-svc.ts +174 -0
- package/src/serve/hub-app/md.ts +15 -0
- package/src/serve/hub-app/media-svc.ts +4 -1
- package/src/serve/hub-app/media-view.tsx +15 -0
- package/src/serve/hub-app/next-run.ts +68 -0
- package/src/serve/hub-app/project-gutter.css +65 -0
- package/src/serve/hub-app/project-gutter.tsx +128 -0
- package/src/serve/hub-app/projects-svc.ts +226 -0
- package/src/serve/hub-app/prose-hits-st.ts +124 -0
- package/src/serve/hub-app/prose-hits-view.tsx +176 -0
- package/src/serve/hub-app/prose-hits.css +21 -0
- package/src/serve/hub-app/run-strip.ts +47 -0
- package/src/serve/hub-app/runs-tabs.ts +24 -0
- package/src/serve/hub-app/test-view.tsx +107 -0
- package/src/serve/hub-app/tests-svc.ts +62 -0
- package/src/serve/hub-app/tests-view.tsx +238 -0
- package/src/serve/hub-app/time-ago.tsx +34 -0
- package/src/serve/hub-app/timeline-chart.tsx +274 -0
- package/src/serve/hub-app/timeline-geometry.ts +314 -0
- package/src/serve/hub-app/timeline-svc.ts +62 -0
- package/src/serve/hub-app/timeline-view.tsx +245 -0
- package/src/serve/hub-app/tree-build.ts +410 -0
- package/src/serve/hub-app/tree-row.tsx +42 -0
- package/src/serve/hub-app/tree-svc.ts +174 -0
- package/src/serve/hub-app/wikis-view.tsx +53 -0
- package/src/serve/other-repos.ts +79 -0
- package/src/serve/project-gutter-route.ts +350 -0
- package/src/serve/project-gutter.ts +449 -0
- package/src/serve/run-serve.ts +245 -16
- package/src/serve/same-origin.ts +14 -0
- package/src/serve/serve-hub.ts +213 -18
- package/src/shipkit.ts +19 -12
- package/src/smoke/run-smoke.ts +3 -2
- package/src/start-repo.ts +75 -36
- package/src/suite/judge-suites.ts +216 -18
- package/src/suite/kept-timings.ts +58 -0
- package/src/suite/runner-shapes.ts +88 -0
- package/src/suite/suite-cli.ts +147 -24
- package/src/suite/suite-cpu-preload.ts +92 -0
- package/src/suite/test-units.ts +243 -0
- package/src/ts/config.ts +4 -69
- package/src/ts/jsonc.ts +69 -0
- package/src/ts/lex.ts +143 -0
- package/src/ts/rename.ts +73 -10
- package/src/ts/source-graph.ts +28 -8
- package/src/ts/tsconfig-projects.ts +6 -97
- package/src/ts/tsconfig-resolve.ts +108 -0
- package/src/tui/build-steps-panel.ts +61 -0
- package/src/tui/checks-panel.ts +24 -16
- package/src/tui/run-tui.ts +2 -2
- package/src/tui/tui-panel.ts +5 -4
- package/src/wiki/graph-colors.ts +68 -0
- package/src/wiki/graph-mount.ts +7 -9
- package/src/wiki/graph-page.ts +42 -25
- package/src/wiki/graph-positions.ts +105 -0
- package/src/wiki/run-wiki.ts +1 -1
- package/src/wiki/serve-graph.ts +33 -3
- package/src/cli/cmd_Rules.ts +0 -51
- package/src/cli/cmd_Units.ts +0 -82
- package/src/collections/graphs.ts +0 -32
- package/src/contracts/contracts-once.ts +0 -43
- package/src/core/check-suite.ts +0 -302
- package/src/core/repair-contract-dirs.ts +0 -136
- package/src/core/task-runner.ts +0 -339
- package/src/ledger/gate-units.ts +0 -138
- package/src/ledger/unit-table.ts +0 -105
- package/src/rules/SK002-contracts-fulfillness.ts +0 -24
- package/src/rules/SK003-claude-md-shape.ts +0 -70
- package/src/rules/SK004-gitignore-policy.ts +0 -238
- package/src/rules/SK010-undeclared-release-unit.ts +0 -127
- package/src/rules/rule-registry.ts +0 -64
- package/src/rules/run-fix.ts +0 -72
- package/src/tui/tasks-panel.ts +0 -60
package/src/ledger/git-key.ts
CHANGED
|
@@ -1,22 +1,30 @@
|
|
|
1
|
-
// THE KEY. What question did this
|
|
1
|
+
// THE KEY. What question did this check answer, and is that still the same
|
|
2
2
|
// question? Nothing in this file caches anything: it produces the identity a
|
|
3
3
|
// later ledger will store results under, and it refuses to produce one
|
|
4
4
|
// whenever the answer would be a guess.
|
|
5
5
|
//
|
|
6
|
-
//
|
|
6
|
+
// FOUR POPULATIONS, set at registration, never inferred from source:
|
|
7
7
|
//
|
|
8
8
|
// CONTENT declares `inputs` — the paths it reads. Keyed on their git object
|
|
9
|
-
// ids, plus the
|
|
9
|
+
// ids, plus the check's own code (module graph), plus the toolchain
|
|
10
10
|
// (which content-hashes the gate binary, so editing shipkit moves
|
|
11
11
|
// every key in every repo), plus argv and declared env. No TTL: a
|
|
12
12
|
// content key is either the same question or a different one.
|
|
13
13
|
// WORLD declares `ttl` — it reads the network, the keychain, $PATH, a git
|
|
14
14
|
// remote or the clock. Nothing on disk describes its answer, so it
|
|
15
15
|
// keeps the existing clean-pass TTL and its `cacheKey`, unchanged.
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
16
|
+
// READS a `run` check file that declares none of the three: the default
|
|
17
|
+
// for that shape. Its body reads through the check context, which
|
|
18
|
+
// records every read, so the key here is only the question (check,
|
|
19
|
+
// code, toolchain, tier) and the ledger stores the recorded read set
|
|
20
|
+
// beside the answer, re-checking it before any replay
|
|
21
|
+
// (ledger/ledger.ts). One whose code reaches the disk or a
|
|
22
|
+
// process around the context stays NEITHER (core/raw-access.ts).
|
|
23
|
+
// NEITHER a `cmd` check or a legacy shape that declares nothing, or any
|
|
24
|
+
// check that says `never`. Never cached. For those it is the
|
|
25
|
+
// DEFAULT, and it is the whole safety story: forgetting to declare
|
|
26
|
+
// costs speed, never correctness, so the burn-down list below is a
|
|
27
|
+
// to-do rather than a hazard. It holds only while nothing declares ON a check's behalf:
|
|
20
28
|
// the gate floor gives `['.']` to the four slots tracked source
|
|
21
29
|
// decides and deliberately leaves `test` here (core/floor.ts says
|
|
22
30
|
// why). Declaring BOTH is a registration failure, loud, because the
|
|
@@ -39,40 +47,48 @@ import { installIdentity, toolchainIdentity } from './toolchain-identity.ts'
|
|
|
39
47
|
|
|
40
48
|
/** the key format. Bumped when a component is added or its rendering changes,
|
|
41
49
|
* so an old ledger can never answer a new question. */
|
|
42
|
-
export const KEY_VERSION =
|
|
50
|
+
export const KEY_VERSION = 3
|
|
43
51
|
|
|
44
|
-
export type Population = 'content' | 'world' | 'none'
|
|
52
|
+
export type Population = 'content' | 'world' | 'reads' | 'none'
|
|
45
53
|
|
|
46
|
-
/** what one gate
|
|
47
|
-
*
|
|
48
|
-
export type
|
|
49
|
-
/** the id an operator types
|
|
54
|
+
/** what one gate check declares about its own cacheability.
|
|
55
|
+
* `gate-checks.ts` builds these from the one enumeration. */
|
|
56
|
+
export type CheckDecl = {
|
|
57
|
+
/** the id an operator types, unique in the repo */
|
|
50
58
|
id: string
|
|
51
|
-
|
|
52
|
-
/** CONTENT: repo-relative paths this unit reads. Presence declares the population. */
|
|
59
|
+
/** CONTENT: repo-relative paths this check reads. Presence declares the population. */
|
|
53
60
|
inputs?: string[]
|
|
54
61
|
/** WORLD: clean-pass duration, e.g. '24h'. Presence declares the population. */
|
|
55
62
|
ttl?: string
|
|
56
|
-
/** repo-relative entry files of the
|
|
63
|
+
/** repo-relative entry files of the check's OWN code — the module-graph
|
|
57
64
|
* roots. A discovered check gets its own file for free. */
|
|
58
65
|
code?: string[]
|
|
59
|
-
/** the argv a subprocess
|
|
66
|
+
/** the argv a subprocess check runs */
|
|
60
67
|
argv?: string[]
|
|
61
68
|
/** env var NAMES whose values belong in the key */
|
|
62
69
|
env?: string[]
|
|
63
70
|
/** anything else that identifies the question (a rule's `cacheKey`) */
|
|
64
71
|
extra?: string
|
|
65
72
|
/** a DECIDED never-cacheable, with the reason. Not the same thing as an
|
|
66
|
-
* undeclared
|
|
73
|
+
* undeclared check: this one has been looked at and cannot be described
|
|
67
74
|
* (it reads git history, a path outside the repo, the wall clock), so it
|
|
68
75
|
* leaves the burn-down list. Declaring it beside inputs or ttl is a
|
|
69
76
|
* registration failure, because those say the opposite. */
|
|
70
77
|
never?: string
|
|
78
|
+
/** it rewrites the repo unless the run is frozen, so frozen is part of its
|
|
79
|
+
* question (check.ts `writes`) */
|
|
80
|
+
writes?: boolean
|
|
81
|
+
/** READS: an in process body whose reads the check context records. Set
|
|
82
|
+
* by the composer for a `run` check file declaring no other population,
|
|
83
|
+
* never by a repo, and ignored beside `inputs`, `ttl` or `never` */
|
|
84
|
+
reads?: boolean
|
|
85
|
+
/** why an undeclared `run` check stays uncached: the first module its body
|
|
86
|
+
* can reach that touches the disk or a process around the context */
|
|
87
|
+
uncacheable?: string
|
|
71
88
|
}
|
|
72
89
|
|
|
73
|
-
export type
|
|
90
|
+
export type CheckKey = {
|
|
74
91
|
id: string
|
|
75
|
-
kind: UnitDecl['kind']
|
|
76
92
|
population: Population
|
|
77
93
|
/** the content key, or null for world/none/refused */
|
|
78
94
|
key: string | null
|
|
@@ -81,50 +97,52 @@ export type UnitKey = {
|
|
|
81
97
|
/** every component that went into the key, in key order — this is what a
|
|
82
98
|
* human reads when a key moved and nobody knows why */
|
|
83
99
|
components: string[]
|
|
84
|
-
/** the input paths this
|
|
100
|
+
/** the input paths this check was keyed on, with their object ids */
|
|
85
101
|
inputOids: [string, string][]
|
|
86
|
-
/** the identity of the
|
|
87
|
-
* every
|
|
102
|
+
/** the identity of the check's OWN code, module graph included. Set for
|
|
103
|
+
* every check that has code, WORLD ones too: a TTL'd probe whose source
|
|
88
104
|
* changed must not replay its previous answer, and this is the value that
|
|
89
|
-
* busts it. null when the
|
|
105
|
+
* busts it. null when the check has no code entry or git could not hash it. */
|
|
90
106
|
codeKey: string | null
|
|
91
107
|
}
|
|
92
108
|
|
|
93
109
|
export type Registration = {
|
|
94
|
-
/** loud, blocking: a
|
|
110
|
+
/** loud, blocking: a check that declared two contradictory populations */
|
|
95
111
|
errors: string[]
|
|
96
|
-
/** the default-deny burn-down, one line per undeclared
|
|
112
|
+
/** the default-deny burn-down, one line per undeclared check */
|
|
97
113
|
burnDown: string[]
|
|
98
|
-
/** every
|
|
99
|
-
|
|
100
|
-
/**
|
|
114
|
+
/** every check that was classified */
|
|
115
|
+
checks: number
|
|
116
|
+
/** checks in a cacheable population (content or world) */
|
|
101
117
|
declared: number
|
|
102
118
|
}
|
|
103
119
|
|
|
104
|
-
/** which population a declaration puts a
|
|
105
|
-
* `inputs` on a `ttl`
|
|
120
|
+
/** which population a declaration puts a check in, before any git work.
|
|
121
|
+
* `inputs` on a `ttl` check is a registration FAIL: a check cannot both be
|
|
106
122
|
* described by files on disk and be un-describable by them. */
|
|
107
|
-
export function classify(d:
|
|
123
|
+
export function classify(d: CheckDecl): { population: Population; error?: string; reason?: string; decided?: boolean } {
|
|
108
124
|
const hasInputs = (d.inputs ?? []).length > 0
|
|
109
125
|
const hasTtl = d.ttl != null && d.ttl !== ''
|
|
110
126
|
const hasNever = d.never != null && d.never !== ''
|
|
111
127
|
if (hasInputs && hasTtl)
|
|
112
128
|
return {
|
|
113
129
|
population: 'none',
|
|
114
|
-
error:
|
|
130
|
+
error: `check '${d.id}' declares BOTH inputs (CONTENT) and ttl (WORLD) — a check reads files or it reads the world, never both`,
|
|
115
131
|
}
|
|
116
132
|
if (hasNever && (hasInputs || hasTtl))
|
|
117
133
|
return {
|
|
118
134
|
population: 'none',
|
|
119
|
-
error:
|
|
135
|
+
error: `check '${d.id}' declares never:'${d.never}' AND ${hasInputs ? 'inputs' : 'ttl'} — never means nothing can describe this check, which the other declaration contradicts`,
|
|
120
136
|
}
|
|
121
137
|
if (hasInputs) return { population: 'content' }
|
|
122
138
|
if (hasTtl) return { population: 'world' }
|
|
123
139
|
if (hasNever) return { population: 'none', reason: d.never, decided: true }
|
|
140
|
+
if (d.reads === true) return { population: 'reads' }
|
|
141
|
+
if (d.uncacheable != null) return { population: 'none', reason: d.uncacheable }
|
|
124
142
|
return { population: 'none', reason: 'declares neither inputs nor ttl' }
|
|
125
143
|
}
|
|
126
144
|
|
|
127
|
-
export function validateRegistration(decls:
|
|
145
|
+
export function validateRegistration(decls: CheckDecl[]): Registration {
|
|
128
146
|
const errors: string[] = []
|
|
129
147
|
const burnDown: string[] = []
|
|
130
148
|
let declared = 0
|
|
@@ -134,19 +152,19 @@ export function validateRegistration(decls: UnitDecl[]): Registration {
|
|
|
134
152
|
// a DECIDED never leaves the list: the burn-down is what nobody has
|
|
135
153
|
// looked at yet, and mixing the two turns it into wallpaper
|
|
136
154
|
else if (c.population !== 'none') declared++
|
|
137
|
-
else if (c.decided !== true) burnDown.push(`${d.
|
|
155
|
+
else if (c.decided !== true) burnDown.push(`${d.id} — ${c.reason ?? 'undeclared'}`)
|
|
138
156
|
// a declaration that cannot even be turned into a repo-relative path
|
|
139
|
-
// is a registration bug, not a cache miss: it would key the
|
|
157
|
+
// is a registration bug, not a cache miss: it would key the check on a
|
|
140
158
|
// path in another repo, or on the repo root
|
|
141
159
|
for (const raw of [...(d.inputs ?? []), ...(d.code ?? [])]) {
|
|
142
160
|
try {
|
|
143
161
|
normalizeInput(raw)
|
|
144
162
|
} catch (e) {
|
|
145
|
-
errors.push(
|
|
163
|
+
errors.push(`check '${d.id}': ${e instanceof Error ? e.message : String(e)}`)
|
|
146
164
|
}
|
|
147
165
|
}
|
|
148
166
|
}
|
|
149
|
-
return { errors, burnDown,
|
|
167
|
+
return { errors, burnDown, checks: decls.length, declared }
|
|
150
168
|
}
|
|
151
169
|
|
|
152
170
|
export type KeyLayerOpts = {
|
|
@@ -159,17 +177,17 @@ export type KeyLayerOpts = {
|
|
|
159
177
|
|
|
160
178
|
/**
|
|
161
179
|
* one scan, N keys. Build it once per run: the git work is shared across every
|
|
162
|
-
*
|
|
180
|
+
* check, and building two of them in one run would pay for the scan twice and
|
|
163
181
|
* could straddle an edit.
|
|
164
182
|
*/
|
|
165
183
|
export class KeyLayer {
|
|
166
184
|
private constructor(
|
|
167
185
|
readonly root: string,
|
|
168
|
-
readonly decls:
|
|
186
|
+
readonly decls: CheckDecl[],
|
|
169
187
|
readonly registration: Registration,
|
|
170
188
|
private readonly scan: ScanResult | null,
|
|
171
189
|
private readonly scanError: string | null,
|
|
172
|
-
private readonly
|
|
190
|
+
private readonly closureByCheck: Map<string, string[]>,
|
|
173
191
|
private readonly codeOids: Map<string, string>,
|
|
174
192
|
private readonly toolchain: { id: string; uncacheable: string[] },
|
|
175
193
|
private readonly scanPathsUsed: string[],
|
|
@@ -183,7 +201,7 @@ export class KeyLayer {
|
|
|
183
201
|
private readonly installIds = new Map<string, string>()
|
|
184
202
|
|
|
185
203
|
/** the identity of the program this run is driving: bun version, the gate
|
|
186
|
-
* binary's whole source, and every installed package. A WORLD
|
|
204
|
+
* binary's whole source, and every installed package. A WORLD check needs
|
|
187
205
|
* it too, because a TTL'd answer measured by a different shipkit is a
|
|
188
206
|
* different answer, and the TTL alone cannot see that. */
|
|
189
207
|
get toolchainId(): string {
|
|
@@ -199,7 +217,7 @@ export class KeyLayer {
|
|
|
199
217
|
return this.scanError
|
|
200
218
|
}
|
|
201
219
|
|
|
202
|
-
static open(root: string, decls:
|
|
220
|
+
static open(root: string, decls: CheckDecl[], o: KeyLayerOpts): KeyLayer {
|
|
203
221
|
const started = performance.now()
|
|
204
222
|
const registration = validateRegistration(decls)
|
|
205
223
|
const cfg = o.cfg ?? loadConfig(root)
|
|
@@ -213,15 +231,15 @@ export class KeyLayer {
|
|
|
213
231
|
toolchain = { id: 'unresolved', uncacheable: [`gate binary unreadable at ${o.cliEntry}: ${e instanceof Error ? e.message : String(e)}`] }
|
|
214
232
|
}
|
|
215
233
|
|
|
216
|
-
// the
|
|
234
|
+
// the check's OWN code, resolved through the same module graph the
|
|
217
235
|
// `cycles` task walks. A check that imports a shared helper carries
|
|
218
236
|
// that helper, so fixing the helper moves the check's key.
|
|
219
|
-
const
|
|
220
|
-
// computed for EVERY
|
|
221
|
-
// world
|
|
237
|
+
const closureByCheck = new Map<string, string[]>()
|
|
238
|
+
// computed for EVERY check with code, not only the content ones: a
|
|
239
|
+
// world check's TTL cache must break when the probe itself is rewritten
|
|
222
240
|
for (const d of decls) {
|
|
223
241
|
const entries = d.code ?? []
|
|
224
|
-
if (entries.length > 0)
|
|
242
|
+
if (entries.length > 0) closureByCheck.set(d.id, importClosure(cfg, entries))
|
|
225
243
|
}
|
|
226
244
|
|
|
227
245
|
// ONE scan for the declared inputs plus shipkit's own source: those
|
|
@@ -237,7 +255,7 @@ export class KeyLayer {
|
|
|
237
255
|
// goes through one `hash-object --stdin-paths` instead of one `git
|
|
238
256
|
// add` spawn per file (measured: 15ms for the whole set)
|
|
239
257
|
const closureFiles = new Set<string>()
|
|
240
|
-
for (const files of
|
|
258
|
+
for (const files of closureByCheck.values()) for (const f of files) closureFiles.add(f)
|
|
241
259
|
|
|
242
260
|
let scan: ScanResult | null = null
|
|
243
261
|
let codeOids = new Map<string, string>()
|
|
@@ -250,28 +268,45 @@ export class KeyLayer {
|
|
|
250
268
|
// downstream may key on it. Loud, and the whole run goes uncached.
|
|
251
269
|
scanError = e instanceof GitScanError ? e.message : String(e)
|
|
252
270
|
}
|
|
253
|
-
return new KeyLayer(root, decls, registration, scan, scanError,
|
|
271
|
+
return new KeyLayer(root, decls, registration, scan, scanError, closureByCheck, codeOids, toolchain, [...paths].sort(), performance.now() - started)
|
|
254
272
|
}
|
|
255
273
|
|
|
256
|
-
/** the key for one
|
|
257
|
-
keyFor(id: string):
|
|
274
|
+
/** the key for one check, or the reason there is none. */
|
|
275
|
+
keyFor(id: string): CheckKey {
|
|
258
276
|
const d = this.decls.find((u) => u.id === id)
|
|
259
|
-
if (d == null) throw new Error(`keyFor: no
|
|
277
|
+
if (d == null) throw new Error(`keyFor: no check '${id}' in this layer`)
|
|
260
278
|
const c = classify(d)
|
|
261
|
-
const own = (this.
|
|
279
|
+
const own = (this.closureByCheck.get(d.id) ?? []).slice().sort()
|
|
262
280
|
const ownReadable = own.every((p) => this.codeOids.get(p) != null)
|
|
263
281
|
const codeLines = own.map((p) => `code:${p}=${this.codeOids.get(p)!}`)
|
|
264
|
-
const base:
|
|
265
|
-
id: d.id,
|
|
282
|
+
const base: CheckKey = {
|
|
283
|
+
id: d.id, population: c.population, key: null, reason: null, components: [], inputOids: [],
|
|
266
284
|
codeKey: own.length === 0 || !ownReadable ? null : createHash('sha256').update(codeLines.join('\n')).digest('hex').slice(0, 16),
|
|
267
285
|
}
|
|
268
286
|
if (c.error != null) return { ...base, reason: c.error }
|
|
269
|
-
if (c.population === 'world') return { ...base, reason: `world
|
|
287
|
+
if (c.population === 'world') return { ...base, reason: `world check — TTL ${d.ttl}, no content key` }
|
|
270
288
|
if (c.population === 'none') return { ...base, reason: c.reason ?? 'undeclared' }
|
|
271
289
|
if (this.scanError != null) return { ...base, reason: `git scan failed, nothing may be keyed: ${this.scanError}` }
|
|
272
290
|
const scan = this.scan!
|
|
273
291
|
if (this.toolchain.uncacheable.length > 0)
|
|
274
292
|
return { ...base, reason: `toolchain not describable: ${this.toolchain.uncacheable.join('; ')}` }
|
|
293
|
+
// the question alone: what it read is stored with each answer and
|
|
294
|
+
// re-checked before a replay, so no read belongs in this key
|
|
295
|
+
if (c.population === 'reads') {
|
|
296
|
+
// a built-in has no code entry: the toolchain id hashes its source
|
|
297
|
+
if (!ownReadable) return { ...base, reason: `git could not hash this check's own source: ${own.filter((p) => this.codeOids.get(p) == null).join(', ')}` }
|
|
298
|
+
const components = [
|
|
299
|
+
`v=${KEY_VERSION}`,
|
|
300
|
+
`check=${d.id}`,
|
|
301
|
+
`bun=${Bun.version}`,
|
|
302
|
+
...(d.writes === true ? [`frozen=${process.env['SHIPKIT_FROZEN'] === '1'}`] : []),
|
|
303
|
+
`toolchain=${this.toolchain.id}`,
|
|
304
|
+
'reads=recorded',
|
|
305
|
+
...codeLines,
|
|
306
|
+
...(d.extra != null ? [`extra=${d.extra}`] : []),
|
|
307
|
+
]
|
|
308
|
+
return { ...base, key: createHash('sha256').update(components.join('\n')).digest('hex').slice(0, 32), components }
|
|
309
|
+
}
|
|
275
310
|
|
|
276
311
|
let inputs: string[]
|
|
277
312
|
try {
|
|
@@ -285,15 +320,17 @@ export class KeyLayer {
|
|
|
285
320
|
const inputOids: [string, string][] = inputs.map((p) => [p, scan.oidByPath.get(p) ?? ''])
|
|
286
321
|
if (inputOids.some(([, oid]) => oid === '')) return { ...base, reason: 'git could not name one of the declared inputs' }
|
|
287
322
|
|
|
288
|
-
if (!ownReadable) return { ...base, reason: `git could not hash this
|
|
323
|
+
if (!ownReadable) return { ...base, reason: `git could not hash this check's own source: ${own.filter((p) => this.codeOids.get(p) == null).join(', ')}` }
|
|
289
324
|
const components: string[] = [
|
|
290
325
|
`v=${KEY_VERSION}`,
|
|
291
|
-
`
|
|
326
|
+
`check=${d.id}`,
|
|
292
327
|
`bun=${Bun.version}`,
|
|
293
|
-
// report-only mode changes what a
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
|
|
328
|
+
// report-only mode changes what a check that WRITES does, so for
|
|
329
|
+
// that check a frozen answer replayed into a normal run, or the
|
|
330
|
+
// reverse, answers the other question. Every other check does the
|
|
331
|
+
// same thing either way, and keying it on the mode only made a
|
|
332
|
+
// frozen run and a normal one measure it twice.
|
|
333
|
+
...(d.writes === true ? [`frozen=${process.env['SHIPKIT_FROZEN'] === '1'}`] : []),
|
|
297
334
|
`toolchain=${this.toolchain.id}`,
|
|
298
335
|
`argv=${JSON.stringify(d.argv ?? [])}`,
|
|
299
336
|
...(d.env ?? []).slice().sort().map((n) => `env:${n}=${process.env[n] ?? '<unset>'}`),
|
|
@@ -305,7 +342,7 @@ export class KeyLayer {
|
|
|
305
342
|
// questions, and a silently narrowed key is the false green.
|
|
306
343
|
//
|
|
307
344
|
// An excluded INSTALL carries its contents rather than only its
|
|
308
|
-
// name.
|
|
345
|
+
// name. A unit's test check declares its dir, and the unit's
|
|
309
346
|
// `node_modules` sits inside it and is ignored, and the toolchain
|
|
310
347
|
// component describes the ROOT install only: without this, a lib
|
|
311
348
|
// whose own dependencies moved replayed a green measured against
|
|
@@ -316,7 +353,7 @@ export class KeyLayer {
|
|
|
316
353
|
return { ...base, key, components, inputOids }
|
|
317
354
|
}
|
|
318
355
|
|
|
319
|
-
/** an excluded install's identity, once per layer: several
|
|
356
|
+
/** an excluded install's identity, once per layer: several checks can name
|
|
320
357
|
* the same one, and hashing a `node_modules` twice in a run is pure cost. */
|
|
321
358
|
private installOf(rel: string): string {
|
|
322
359
|
let id = this.installIds.get(rel)
|
|
@@ -324,22 +361,22 @@ export class KeyLayer {
|
|
|
324
361
|
return id
|
|
325
362
|
}
|
|
326
363
|
|
|
327
|
-
keys():
|
|
364
|
+
keys(): CheckKey[] {
|
|
328
365
|
return this.decls.map((d) => this.keyFor(d.id))
|
|
329
366
|
}
|
|
330
367
|
|
|
331
|
-
/** which
|
|
368
|
+
/** which checks READ one of these paths. Answers the only question a
|
|
332
369
|
* post-run re-hash has: whose result now describes a tree that no longer
|
|
333
|
-
* exists. A
|
|
370
|
+
* exists. A check declaring the whole work tree reads everything, so any
|
|
334
371
|
* drift at all touches it. */
|
|
335
372
|
unitsTouchedBy(drifted: string[]): Set<string> {
|
|
336
373
|
const hit = new Set<string>()
|
|
337
374
|
if (drifted.length === 0) return hit
|
|
338
375
|
for (const d of this.decls) {
|
|
339
|
-
// the
|
|
376
|
+
// the check's own source counts exactly like a declared input: an
|
|
340
377
|
// agent fixing a check WHILE it runs must not store the old
|
|
341
378
|
// verdict under the new code's key
|
|
342
|
-
if ((this.
|
|
379
|
+
if ((this.closureByCheck.get(d.id) ?? []).some((f) => drifted.includes(f))) { hit.add(d.id); continue }
|
|
343
380
|
for (const raw of d.inputs ?? []) {
|
|
344
381
|
let p: string
|
|
345
382
|
try { p = normalizeInput(raw) } catch { hit.add(d.id); break }
|
|
@@ -351,24 +388,26 @@ export class KeyLayer {
|
|
|
351
388
|
|
|
352
389
|
/**
|
|
353
390
|
* POST RUN RE HASH. Re-scan exactly what this layer keyed on and report
|
|
354
|
-
* every path whose object id moved while the
|
|
391
|
+
* every path whose object id moved while the checks were running.
|
|
355
392
|
*
|
|
356
|
-
* This is the answer to the poisoning class: a
|
|
393
|
+
* This is the answer to the poisoning class: a check reads a file, an agent
|
|
357
394
|
* in the next window edits it, and the result gets stored under a key that
|
|
358
395
|
* describes the file as it was BEFORE the edit. Any drift here means the
|
|
359
396
|
* run's results describe a tree that no longer exists, and the caller
|
|
360
|
-
* discards them rather than storing a lie.
|
|
361
|
-
*
|
|
397
|
+
* discards them rather than storing a lie. It reuses the first scan's
|
|
398
|
+
* ignored answer, and an unchanged root tree answers every path without
|
|
399
|
+
* asking git per path: about 60ms on this repo when nothing moved, which
|
|
400
|
+
* is the price of never having to explain a hit that was wrong.
|
|
362
401
|
*/
|
|
363
402
|
rescan(): { drifted: string[]; error: string | null; ms: number } {
|
|
364
403
|
if (this.scanError != null) return { drifted: [], error: this.scanError, ms: 0 }
|
|
365
404
|
const started = performance.now()
|
|
366
405
|
try {
|
|
367
|
-
const after = scanPaths(this.root, this.scanPathsUsed)
|
|
406
|
+
const after = scanPaths(this.root, this.scanPathsUsed, this.scan!)
|
|
368
407
|
const drifted: string[] = []
|
|
369
408
|
for (const [p, oid] of this.scan!.oidByPath) if (after.oidByPath.get(p) !== oid) drifted.push(p)
|
|
370
409
|
for (const [p] of after.oidByPath) if (!this.scan!.oidByPath.has(p)) drifted.push(p)
|
|
371
|
-
// the
|
|
410
|
+
// the check's own code drifts the same way a declared input does: an
|
|
372
411
|
// agent fixing a check WHILE that check runs would otherwise store
|
|
373
412
|
// the old verdict under the new code's key
|
|
374
413
|
const files = [...this.codeOids.keys()]
|