rvlib-shipkit 1.4.0 → 1.6.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 +114 -0
- package/README.md +17 -17
- package/package.json +3 -2
- package/src/assets/gen/comfyts-gen.ts +12 -8
- package/src/assets/server/studio-server.ts +14 -2
- package/src/check.ts +73 -103
- package/src/checks/SK001-package-json-shape.check.ts +11 -6
- package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
- package/src/checks/SK003-claude-md-shape.check.ts +7 -3
- package/src/checks/SK004-gitignore-policy.check.ts +7 -4
- package/src/checks/SK005-rvlib-versions.check.ts +53 -21
- package/src/checks/SK006-npm-publish-safety.check.ts +4 -0
- package/src/checks/SK007-github-visibility.check.ts +4 -2
- package/src/checks/SK008-reviewer-agent.check.ts +6 -4
- package/src/checks/SK009-commit-ai-tells.check.ts +4 -2
- package/src/checks/SK010-undeclared-unit.check.ts +4 -0
- package/src/checks/SK011-sibling-unit-ranges.check.ts +4 -0
- package/src/checks/SK012-config-barrel-import.check.ts +5 -1
- package/src/checks/SK013-one-way-in-policy.check.ts +7 -3
- package/src/checks/SK014-prose-ste.check.ts +4 -0
- package/src/checks/SK015-nested-repo.check.ts +4 -2
- package/src/checks/SK016-legacy-check-shapes.check.ts +13 -12
- package/src/checks/SK017-vscode-hub-task.check.ts +22 -7
- package/src/checks/SK018-test-files-run.check.ts +6 -2
- package/src/checks/SK019-tests-run-in-units.check.ts +8 -4
- package/src/checks/SK020-self-spawning-check.check.ts +4 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -4
- package/src/checks/SK024-identity-colors.check.ts +5 -3
- package/src/checks/SK025-release-drift.check.ts +4 -5
- package/src/checks/SK026-asks-autoclose.check.ts +4 -4
- package/src/checks/SK027-agent-wiki-links.check.ts +4 -1
- package/src/checks/SK028-readme.check.ts +5 -1
- package/src/checks/SK029-explicit-check-files.check.ts +95 -0
- package/src/checks/SK030-cache-audit.check.ts +34 -0
- package/src/checks/SK031-trace-audit.check.ts +39 -0
- package/src/checks/SK032-ignored-inputs.check.ts +79 -0
- package/src/cli/cmd_Asks.ts +5 -5
- package/src/cli/cmd_Assets.ts +17 -13
- package/src/cli/cmd_Bump.ts +12 -11
- package/src/cli/cmd_CacheAudit.ts +43 -0
- package/src/cli/cmd_Check.ts +8 -7
- package/src/cli/cmd_Checks.ts +7 -6
- package/src/cli/cmd_Ci.ts +12 -11
- package/src/cli/cmd_Commands.ts +16 -13
- package/src/cli/cmd_ConfigKeys.ts +7 -6
- package/src/cli/cmd_Contracts.ts +3 -3
- package/src/cli/cmd_CrossRepoActivity.ts +8 -6
- package/src/cli/cmd_Fleet.ts +9 -7
- package/src/cli/cmd_GlobalProcesses.ts +120 -7
- package/src/cli/cmd_Home.ts +26 -19
- package/src/cli/cmd_Hooks.ts +6 -5
- package/src/cli/cmd_Init.ts +5 -4
- package/src/cli/cmd_Keys.ts +20 -8
- package/src/cli/cmd_Mv.ts +7 -6
- package/src/cli/cmd_Needs.ts +4 -3
- package/src/cli/cmd_Overview.ts +5 -4
- package/src/cli/cmd_Pb.ts +2 -2
- package/src/cli/cmd_Planned.ts +6 -5
- package/src/cli/cmd_Prose.ts +5 -5
- package/src/cli/cmd_Reflect.ts +7 -7
- package/src/cli/cmd_Release.ts +6 -5
- package/src/cli/cmd_Releases.ts +5 -4
- package/src/cli/cmd_Serve.ts +6 -4
- package/src/cli/cmd_Smoke.ts +5 -4
- package/src/cli/cmd_Start.ts +7 -6
- package/src/cli/cmd_Status.ts +4 -3
- package/src/cli/cmd_TraceAudit.ts +62 -0
- package/src/cli/cmd_Tui.ts +3 -2
- package/src/cli/cmd_When.ts +71 -0
- package/src/cli/cmd_Wiki.ts +4 -4
- package/src/cli.ts +11 -2
- package/src/collections/checks.ts +2 -0
- package/src/collections/collection-types.ts +12 -4
- package/src/collections/providers.ts +5 -3
- package/src/core/app-icon.ts +154 -0
- package/src/core/audit-report.ts +21 -0
- package/src/core/cache-audit.ts +72 -0
- package/src/core/canonical-scripts.ts +2 -2
- package/src/core/check-ctx.ts +14 -5
- package/src/core/check-runner.ts +43 -23
- package/src/core/config-rewrite.ts +37 -24
- package/src/core/discover-checks.ts +23 -2
- package/src/core/floor-resolve.ts +5 -7
- package/src/core/git-root.ts +13 -0
- package/src/core/ignored-block.ts +94 -0
- package/src/core/ignored-inputs.ts +47 -0
- package/src/core/jobs.ts +9 -2
- package/src/core/legacy-shapes.ts +39 -10
- package/src/core/one-lib.ts +59 -4
- package/src/core/one-way-in-policy.ts +2 -2
- package/src/core/outcome-words.ts +18 -2
- package/src/core/port-check-cli.ts +52 -0
- package/src/core/port-check-file.ts +295 -0
- package/src/core/port-check-files.ts +31 -0
- package/src/core/read-set.ts +97 -9
- package/src/core/retention.ts +0 -14
- package/src/core/run-check.ts +12 -9
- package/src/core/run-ci.ts +14 -5
- package/src/core/run-fix.ts +30 -23
- package/src/core/run-fixers.ts +48 -5
- package/src/core/runs-matrix.ts +179 -0
- package/src/core/runs.ts +242 -0
- package/src/core/trace-audit.ts +120 -0
- package/src/core/update-policy.ts +20 -0
- package/src/core/when-block.ts +29 -30
- package/src/core/when.ts +41 -103
- package/src/core/wire.ts +10 -14
- package/src/fleet/scan-fleet.ts +20 -4
- package/src/fleet/update-fleet.ts +6 -6
- package/src/home/gui-app.ts +24 -27
- package/src/home/home-launchd.ts +20 -4
- package/src/home/home-routes.ts +12 -1
- package/src/home/home-start.ts +11 -5
- package/src/home/hubs.ts +5 -5
- package/src/home/open-app.ts +2 -2
- package/src/home/pm.ts +8 -0
- package/src/home/run-pm.ts +1 -0
- package/src/home/serve-web.ts +2 -2
- package/src/home/supervise.ts +24 -1
- package/src/home/via-hub.ts +14 -0
- package/src/hooks/run-hooks.ts +48 -4
- package/src/init-repo.ts +6 -6
- package/src/ledger/db.ts +2 -13
- package/src/ledger/gate-checks.ts +133 -61
- package/src/ledger/git-key.ts +153 -31
- package/src/ledger/git-scan.ts +5 -2
- package/src/ledger/ledger.ts +136 -21
- package/src/ledger/store.ts +14 -0
- package/src/ledger/toolchain-identity.ts +248 -45
- package/src/plan/repo-source.ts +18 -39
- package/src/problems/extract.ts +3 -0
- package/src/release/run-release.ts +5 -0
- package/src/repo-config.ts +46 -54
- package/src/rules/rule.ts +2 -2
- package/src/scaffold/files/agent-coding.md +1 -1
- package/src/scaffold/files/agent-index.md +1 -1
- package/src/scaffold/files/verify-skill.md +2 -2
- package/src/serve/hub-app/activity-st.ts +5 -4
- package/src/serve/hub-app/activity-view.tsx +1 -1
- package/src/serve/hub-app/admin-svc.ts +36 -3
- package/src/serve/hub-app/admin-view.tsx +46 -12
- package/src/serve/hub-app/check-view.tsx +66 -46
- package/src/serve/hub-app/check-words.ts +1 -11
- package/src/serve/hub-app/checks-svc.ts +14 -35
- package/src/serve/hub-app/col-card.tsx +1 -44
- package/src/serve/hub-app/col-tables-view.tsx +84 -11
- package/src/serve/hub-app/col-tables.ts +27 -0
- package/src/serve/hub-app/cols-svc.ts +5 -4
- package/src/serve/hub-app/dock-st.ts +3 -2
- package/src/serve/hub-app/dock-view.tsx +36 -0
- package/src/serve/hub-app/dock.css +6 -0
- package/src/serve/hub-app/float-cards.tsx +30 -0
- package/src/serve/hub-app/floating.tsx +41 -4
- package/src/serve/hub-app/git-st.ts +42 -0
- package/src/serve/hub-app/git-view.tsx +61 -0
- package/src/serve/hub-app/hash-route.ts +45 -1
- package/src/serve/hub-app/header-bar.tsx +62 -72
- package/src/serve/hub-app/health-svc.ts +3 -2
- package/src/serve/hub-app/home-st.ts +14 -2
- package/src/serve/hub-app/home-strip.css +43 -12
- package/src/serve/hub-app/home-strip.tsx +115 -64
- package/src/serve/hub-app/home.html +1 -0
- package/src/serve/hub-app/hub-app.tsx +32 -8
- package/src/serve/hub-app/hub-types.ts +5 -2
- package/src/serve/hub-app/hub.css +89 -63
- package/src/serve/hub-app/icons.tsx +11 -0
- package/src/serve/hub-app/index.html +1 -0
- package/src/serve/hub-app/keys.ts +15 -4
- package/src/serve/hub-app/live-svc.ts +3 -2
- package/src/serve/hub-app/machine-st.ts +18 -22
- package/src/serve/hub-app/plan-view.tsx +51 -0
- package/src/serve/hub-app/planned-st.ts +9 -4
- package/src/serve/hub-app/processes-view.tsx +45 -0
- package/src/serve/hub-app/project-list.css +4 -1
- package/src/serve/hub-app/project-list.tsx +6 -3
- package/src/serve/hub-app/projects-svc.ts +25 -10
- package/src/serve/hub-app/prose-hits-st.ts +3 -2
- package/src/serve/hub-app/reach.ts +36 -0
- package/src/serve/hub-app/readme-svc.ts +3 -2
- package/src/serve/hub-app/repo-sections.ts +131 -0
- package/src/serve/hub-app/repo-st.ts +70 -14
- package/src/serve/hub-app/sidebar.tsx +17 -42
- package/src/serve/hub-app/timeline-svc.ts +3 -2
- package/src/serve/hub-app/tree-build.ts +151 -155
- package/src/serve/hub-app/tree-row.tsx +55 -12
- package/src/serve/hub-app/tree-svc.ts +97 -56
- package/src/serve/hub-app/when-view.tsx +89 -0
- package/src/serve/hub-app/when.css +17 -0
- package/src/serve/hub-runtime.ts +7 -18
- package/src/serve/project-gutter-route.ts +22 -1
- package/src/serve/run-serve.ts +1 -1
- package/src/serve/serve-hub.ts +4 -14
- package/src/suite/judge-suites.ts +6 -0
- package/src/trace/interpose.c +655 -0
- package/src/trace/read-trace.ts +485 -0
- package/src/trace/tracer.ts +282 -0
- package/src/serve/hub-app/processes-svc.ts +0 -63
- package/src/serve/hub-app/repo-sections.tsx +0 -61
- package/src/serve/repo-processes.ts +0 -89
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// WHAT PROGRAM IS ABOUT TO RUN, as
|
|
1
|
+
// WHAT PROGRAM IS ABOUT TO RUN, as hashes.
|
|
2
2
|
//
|
|
3
3
|
// A cache key built from the repo's git tree answers "what source is being
|
|
4
4
|
// gated". It cannot answer "what is doing the gating", and those two already
|
|
@@ -16,8 +16,12 @@
|
|
|
16
16
|
// package.json files of exactly the same byte length, so only the parsed
|
|
17
17
|
// `version` separates them.
|
|
18
18
|
//
|
|
19
|
-
// So
|
|
20
|
-
//
|
|
19
|
+
// So there are three parts: the RUNNER (the runtime, the gate code that runs
|
|
20
|
+
// and stores every answer, the data files a check reads), the INSTALL (what is
|
|
21
|
+
// actually in node_modules), and the toolkit SOURCE, which each check is keyed
|
|
22
|
+
// on through its own module closure (LibSource), never as a whole. A whole
|
|
23
|
+
// tree hash made a css edit in the hub, or a new test file, move every key in
|
|
24
|
+
// every repo on the machine, and a repo that links the toolkit paid it twice.
|
|
21
25
|
//
|
|
22
26
|
// THE ONE DELIBERATE MISS: a human hand-editing a file inside `node_modules`
|
|
23
27
|
// is accepted, not caught. Catching it means content-hashing the whole tree,
|
|
@@ -26,16 +30,22 @@
|
|
|
26
30
|
// cache slower than the gate it caches. No tool produces that state; a human
|
|
27
31
|
// debugging on purpose can delete the ledger.
|
|
28
32
|
import { createHash } from 'crypto'
|
|
29
|
-
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from 'fs'
|
|
30
|
-
import { isAbsolute, join, relative, resolve } from 'path'
|
|
33
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync, statSync } from 'fs'
|
|
34
|
+
import { dirname, isAbsolute, join, posix, relative, resolve } from 'path'
|
|
35
|
+
import { ALWAYS_SKIP, type RvtConfig } from '../ts/config.ts'
|
|
36
|
+
import { extractSpecs, resolveSpec } from '../ts/source-graph.ts'
|
|
37
|
+
import { blobOid } from '../core/read-set.ts'
|
|
31
38
|
|
|
32
39
|
export type Toolchain = {
|
|
33
|
-
/** the
|
|
40
|
+
/** the RUNNER, 16 hex chars: the runtime, the gate code that runs and
|
|
41
|
+
* stores every answer, and the data files a check reads. Every key
|
|
42
|
+
* carries it */
|
|
34
43
|
id: string
|
|
35
|
-
/**
|
|
36
|
-
*
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
/** what is installed, 16 hex chars. Only a key whose answer runs the
|
|
45
|
+
* repo's own tools carries it: a content or a world check */
|
|
46
|
+
install: string
|
|
47
|
+
/** the toolkit's source, for a check's own module closure */
|
|
48
|
+
lib: LibSource
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
/** the gate binary driving THIS process, which is what the key hashes when a
|
|
@@ -62,23 +72,202 @@ function installedNames(nm: string): string[] {
|
|
|
62
72
|
return names.sort()
|
|
63
73
|
}
|
|
64
74
|
|
|
65
|
-
/**
|
|
66
|
-
*
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
/** every file under `dir`, hashed as its path, its size and its bytes in
|
|
76
|
+
* path order, so a rename or a line moved between files moves the hash too.
|
|
77
|
+
* Its own installs and dot entries are left out. A symlink is followed (a
|
|
78
|
+
* cycle is walked once), and one that cannot be read THROWS: a tree only
|
|
79
|
+
* partly read must never be keyed as if it were whole */
|
|
80
|
+
function hashTree(h: ReturnType<typeof createHash>, dir: string, tag: string): void {
|
|
81
|
+
const files: { rel: string; abs: string }[] = []
|
|
82
|
+
const seen = new Set<string>()
|
|
83
|
+
const walk = (d: string, rel: string): void => {
|
|
84
|
+
const real = realpathSync(d)
|
|
85
|
+
if (seen.has(real)) return
|
|
86
|
+
seen.add(real)
|
|
87
|
+
for (const name of readdirSync(d)) {
|
|
88
|
+
if (name === 'node_modules' || name.startsWith('.')) continue
|
|
89
|
+
const abs = join(d, name)
|
|
90
|
+
const r = rel === '' ? name : `${rel}/${name}`
|
|
91
|
+
if (statSync(abs).isDirectory()) walk(abs, r)
|
|
92
|
+
else files.push({ rel: r, abs })
|
|
76
93
|
}
|
|
77
94
|
}
|
|
78
|
-
walk(
|
|
79
|
-
files.sort()
|
|
80
|
-
for (const f of files)
|
|
81
|
-
|
|
95
|
+
walk(dir, '')
|
|
96
|
+
files.sort((a, b) => (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0))
|
|
97
|
+
for (const f of files) {
|
|
98
|
+
const bytes = readFileSync(f.abs)
|
|
99
|
+
h.update(`${f.rel}\0${bytes.length}\0`)
|
|
100
|
+
h.update(bytes)
|
|
101
|
+
}
|
|
102
|
+
h.update(`${tag}=${files.length}\n`)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** a path with its links resolved, as given when it cannot be (a file not
|
|
106
|
+
* there yet): /var and /private/var must name one file */
|
|
107
|
+
const real = (p: string): string => { try { return realpathSync(p) } catch { return p } }
|
|
108
|
+
|
|
109
|
+
/** the modules that run a check and store its answer, relative to the
|
|
110
|
+
* toolkit's src. A gate binary without them (a test fixture) is keyed on its
|
|
111
|
+
* own entry's closure */
|
|
112
|
+
export const RUNNER_ENTRIES = ['core/run-ci.ts', 'core/run-check.ts']
|
|
113
|
+
|
|
114
|
+
/** file kinds under src that no check and no runner reads: the page the hub
|
|
115
|
+
* serves and the mac app's source. Every other non code file there is data a
|
|
116
|
+
* check can read (the scaffold templates), so it is in the runner id */
|
|
117
|
+
const NOT_READ_BY_CHECKS = /\.(css|html|swift)$/
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* the toolkit's source, keyed one module closure at a time. A check is keyed
|
|
121
|
+
* on the files its own module can reach, the runner on the files the runner
|
|
122
|
+
* can reach, and a file neither reaches (the hub page, a test) moves nothing.
|
|
123
|
+
* Each file is hashed once per process.
|
|
124
|
+
*/
|
|
125
|
+
export class LibSource {
|
|
126
|
+
/** the package root, resolved: the dir holding the source dir */
|
|
127
|
+
readonly pkgRoot: string
|
|
128
|
+
/** the source dir, resolved: the gate binary's own dir */
|
|
129
|
+
readonly srcDir: string
|
|
130
|
+
private readonly cfg: RvtConfig
|
|
131
|
+
private readonly oids = new Map<string, string>()
|
|
132
|
+
private readonly closures = new Map<string, string[]>()
|
|
133
|
+
/** file → the files it imports directly, so a file shared by many
|
|
134
|
+
* closures is read and parsed once */
|
|
135
|
+
private readonly direct = new Map<string, string[]>()
|
|
136
|
+
private readonly probed = new Map<string, boolean>()
|
|
137
|
+
/** the package name a repo imports the toolkit by, null for a toolkit
|
|
138
|
+
* with no package.json (a test fixture) */
|
|
139
|
+
readonly name: string | null
|
|
140
|
+
|
|
141
|
+
constructor(cliEntry: string) {
|
|
142
|
+
this.srcDir = dirname(realpathSync(cliEntry))
|
|
143
|
+
this.pkgRoot = dirname(this.srcDir)
|
|
144
|
+
let name: string | null = null
|
|
145
|
+
try { name = (JSON.parse(readFileSync(join(this.pkgRoot, 'package.json'), 'utf8')) as { name?: string }).name ?? null } catch { name = null }
|
|
146
|
+
this.name = name
|
|
147
|
+
// the toolkit imports itself by its package name, and a closure that
|
|
148
|
+
// could not follow those would stop at the first such import
|
|
149
|
+
this.cfg = { root: this.pkgRoot, roots: [relative(this.pkgRoot, this.srcDir)], skip: [...ALWAYS_SKIP], packages: name == null ? [] : [{ name, dir: '.' }] }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** the toolkit file a package specifier names (`rvlib-shipkit/src/x.ts`),
|
|
153
|
+
* or null when it names none */
|
|
154
|
+
fileOf(spec: string): string | null {
|
|
155
|
+
if (this.name == null || !spec.startsWith(`${this.name}/`)) return null
|
|
156
|
+
const abs = join(this.pkgRoot, spec.slice(this.name.length + 1))
|
|
157
|
+
return this.owns(abs) && existsSync(abs) ? abs : null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** is this absolute path a source file of the toolkit? */
|
|
161
|
+
owns(abs: string): boolean {
|
|
162
|
+
const r = relative(this.srcDir, real(abs))
|
|
163
|
+
return r !== '' && !r.startsWith('..') && !isAbsolute(r) && /\.tsx?$/.test(abs)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** package-relative files one entry can reach, dynamic imports included.
|
|
167
|
+
* The same walk as `importClosure`, with each file's imports kept */
|
|
168
|
+
closureOf(abs: string): string[] {
|
|
169
|
+
const rel = relative(this.pkgRoot, real(abs)).replace(/\\/g, '/')
|
|
170
|
+
let c = this.closures.get(rel)
|
|
171
|
+
if (c == null) {
|
|
172
|
+
const seen = new Set<string>([rel])
|
|
173
|
+
const queue = [rel]
|
|
174
|
+
while (queue.length > 0) for (const t of this.importsOf(queue.shift()!)) if (!seen.has(t)) { seen.add(t); queue.push(t) }
|
|
175
|
+
c = [...seen].sort()
|
|
176
|
+
this.closures.set(rel, c)
|
|
177
|
+
}
|
|
178
|
+
return c
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private importsOf(file: string): string[] {
|
|
182
|
+
let d = this.direct.get(file)
|
|
183
|
+
if (d == null) {
|
|
184
|
+
const has = (p: string): boolean => {
|
|
185
|
+
let r = this.probed.get(p)
|
|
186
|
+
if (r == null) { r = /\.tsx?$/.test(p) && existsSync(join(this.pkgRoot, p)); this.probed.set(p, r) }
|
|
187
|
+
return r
|
|
188
|
+
}
|
|
189
|
+
let text = ''
|
|
190
|
+
// the entry itself must exist: a missing one throws below, when it is hashed
|
|
191
|
+
try { text = readFileSync(join(this.pkgRoot, file), 'utf8') } catch { text = '' }
|
|
192
|
+
const resolveHere = (spec: string): string | null => resolveSpec(this.cfg, file, spec, has)
|
|
193
|
+
const targets = [...extractSpecs(text, { dynamic: true }).map(resolveHere), ...pathLiterals(text, file, resolveHere)]
|
|
194
|
+
d = [...new Set(targets.filter((t): t is string => t != null).map((t) => posix.normalize(t)))]
|
|
195
|
+
this.direct.set(file, d)
|
|
196
|
+
}
|
|
197
|
+
return d
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** blob id of one package-relative file. A file that cannot be read
|
|
201
|
+
* THROWS: a closure only partly read must never be keyed */
|
|
202
|
+
private oid(rel: string): string {
|
|
203
|
+
let o = this.oids.get(rel)
|
|
204
|
+
if (o == null) { o = blobOid(readFileSync(join(this.pkgRoot, rel))); this.oids.set(rel, o) }
|
|
205
|
+
return o
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** the identity of what these entries can reach, 16 hex chars */
|
|
209
|
+
closureKey(absEntries: string[]): string {
|
|
210
|
+
const files = [...new Set(absEntries.flatMap((e) => this.closureOf(e)))].sort()
|
|
211
|
+
const h = createHash('sha256')
|
|
212
|
+
for (const f of files) h.update(`${f}=${this.oid(f)}\n`)
|
|
213
|
+
h.update(`n=${files.length}\n`)
|
|
214
|
+
return h.digest('hex').slice(0, 16)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** the data files under src a check can read, as lines */
|
|
218
|
+
dataLines(): string[] {
|
|
219
|
+
const out: string[] = []
|
|
220
|
+
const walk = (dir: string, rel: string): void => {
|
|
221
|
+
for (const name of readdirSync(dir).sort()) {
|
|
222
|
+
if (name === 'node_modules' || name.startsWith('.')) continue
|
|
223
|
+
const abs = join(dir, name)
|
|
224
|
+
const r = `${rel}/${name}`
|
|
225
|
+
if (statSync(abs).isDirectory()) walk(abs, r)
|
|
226
|
+
else if (!/\.tsx?$/.test(name) && !NOT_READ_BY_CHECKS.test(name)) out.push(`data:${r}=${this.oid(r)}`)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
walk(this.srcDir, relative(this.pkgRoot, this.srcDir))
|
|
230
|
+
// the version a check compares a repo's pin against
|
|
231
|
+
if (existsSync(join(this.pkgRoot, 'package.json'))) out.push(`data:package.json=${this.oid('package.json')}`)
|
|
232
|
+
return out
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** toolkit modules a file RUNS by path rather than imports: a preload handed
|
|
237
|
+
* to bun, a cli spawned through `c.exec`. `join(import.meta.dir, 'x.ts')`,
|
|
238
|
+
* `new URL('x.ts', import.meta.url)` and `import.meta.resolve('pkg/x.ts')`,
|
|
239
|
+
* resolved like an import. The gate's own `cli.ts` is left out: a module
|
|
240
|
+
* that names it spawns the whole program, and what of it produces an answer
|
|
241
|
+
* is the runner every key already carries */
|
|
242
|
+
function pathLiterals(text: string, file: string, resolveSpecifier: (spec: string) => string | null): (string | null)[] {
|
|
243
|
+
const out: (string | null)[] = []
|
|
244
|
+
const str = /'([^'\n]+)'|"([^"\n]+)"/g
|
|
245
|
+
const strings = (s: string): string[] => [...s.matchAll(str)].map((x) => x[1] ?? x[2]!)
|
|
246
|
+
const rel = (parts: string[]): string | null => {
|
|
247
|
+
const target = posix.normalize(posix.join(posix.dirname(file), ...parts))
|
|
248
|
+
if (target.endsWith('/cli.ts') || target === 'cli.ts') return null
|
|
249
|
+
return resolveSpecifier(`./${posix.relative(posix.dirname(file), target)}`)
|
|
250
|
+
}
|
|
251
|
+
for (const m of text.matchAll(/join\(\s*import\.meta\.dir\s*,((?:\s*(?:'[^'\n]*'|"[^"\n]*")\s*,?)+)\)/g)) {
|
|
252
|
+
const parts = strings(m[1]!)
|
|
253
|
+
if (parts.length > 0 && /\.tsx?$/.test(parts.at(-1)!)) out.push(rel(parts))
|
|
254
|
+
}
|
|
255
|
+
for (const m of text.matchAll(/new URL\(\s*('[^'\n]+'|"[^"\n]+")\s*,\s*import\.meta\.url\s*\)/g)) {
|
|
256
|
+
const p = strings(m[1]!)[0]!
|
|
257
|
+
if (/\.tsx?$/.test(p)) out.push(rel([p]))
|
|
258
|
+
}
|
|
259
|
+
for (const m of text.matchAll(/import\.meta\.resolve\(\s*('[^'\n]+'|"[^"\n]+")\s*\)/g)) {
|
|
260
|
+
const p = strings(m[1]!)[0]!
|
|
261
|
+
if (!p.endsWith('/cli.ts')) out.push(resolveSpecifier(p))
|
|
262
|
+
}
|
|
263
|
+
return out
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** the runner: the modules that run a check and store its answer, plus the
|
|
267
|
+
* data files a check reads */
|
|
268
|
+
function runnerLines(lib: LibSource, cliEntry: string): string[] {
|
|
269
|
+
const entries = RUNNER_ENTRIES.map((e) => join(lib.srcDir, e)).filter((e) => existsSync(e))
|
|
270
|
+
return [`runner=${lib.closureKey(entries.length > 0 ? entries : [realpathSync(cliEntry)])}`, ...lib.dataLines()]
|
|
82
271
|
}
|
|
83
272
|
|
|
84
273
|
/**
|
|
@@ -118,19 +307,14 @@ export function installIdentity(nodeModules: string): string {
|
|
|
118
307
|
* moves an mtime without changing content produces no miss.
|
|
119
308
|
*/
|
|
120
309
|
export function toolchainIdentity(repoRoot: string, cliEntry: string): Toolchain {
|
|
121
|
-
const h = createHash('sha256')
|
|
122
|
-
const uncacheable: string[] = []
|
|
123
310
|
// BOTH sides resolved, or the comparison is wrong on macOS: /var is a
|
|
124
311
|
// symlink to /private/var, so an in-repo cli under a resolved path reads
|
|
125
312
|
// as outside an unresolved root. That mistake is silent and expensive: it
|
|
126
|
-
//
|
|
127
|
-
//
|
|
313
|
+
// folds absolute paths into the id, which is what would stop two
|
|
314
|
+
// worktrees agreeing.
|
|
128
315
|
const root = ((): string => { try { return realpathSync(repoRoot) } catch { return resolve(repoRoot) } })()
|
|
129
316
|
|
|
130
|
-
// the
|
|
131
|
-
h.update(`bun=${Bun.version}\n`)
|
|
132
|
-
|
|
133
|
-
// the gate binary, ALWAYS content-hashed, and its path never folded in.
|
|
317
|
+
// the gate code, ALWAYS content-hashed, and its path never folded in.
|
|
134
318
|
//
|
|
135
319
|
// The earlier shape wrote `cli=in-repo` for a binary inside the repo and
|
|
136
320
|
// leaned on the git tree key to cover it. Both halves were wrong, and both
|
|
@@ -139,39 +323,58 @@ export function toolchainIdentity(repoRoot: string, cliEntry: string): Toolchain
|
|
|
139
323
|
// covered none of it. And between a checkout that HOSTS the binary and a
|
|
140
324
|
// worktree that runs the same one through the global shim, one wrote
|
|
141
325
|
// `in-repo` and the other wrote an absolute path, so two checkouts running
|
|
142
|
-
// one identical program could never agree on a key.
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
|
|
326
|
+
// one identical program could never agree on a key. Package-relative
|
|
327
|
+
// content is the only form that is the same value wherever the caller
|
|
328
|
+
// stands.
|
|
329
|
+
const lib = new LibSource(cliEntry)
|
|
330
|
+
const r = createHash('sha256')
|
|
331
|
+
// the runtime. Free, and a bun upgrade genuinely changes behaviour
|
|
332
|
+
r.update(`bun=${Bun.version}\n`)
|
|
333
|
+
for (const l of runnerLines(lib, cliEntry)) r.update(`${l}\n`)
|
|
334
|
+
return { id: r.digest('hex').slice(0, 16), install: installOf(root, lib), lib }
|
|
335
|
+
}
|
|
146
336
|
|
|
147
|
-
|
|
337
|
+
/** what is installed. The lockfile states the intent; the tree states the fact */
|
|
338
|
+
function installOf(root: string, lib: LibSource): string {
|
|
339
|
+
const h = createHash('sha256')
|
|
148
340
|
const lock = join(root, 'bun.lock')
|
|
149
341
|
h.update(`lock=${existsSync(lock) ? createHash('sha256').update(readFileSync(lock)).digest('hex') : 'none'}\n`)
|
|
150
342
|
const nm = join(root, 'node_modules')
|
|
151
343
|
if (!existsSync(nm)) {
|
|
152
344
|
// a worktree nobody installed is a REAL difference, not a missing input
|
|
153
345
|
h.update('nm=absent\n')
|
|
154
|
-
return
|
|
346
|
+
return h.digest('hex').slice(0, 16)
|
|
155
347
|
}
|
|
156
348
|
const names = installedNames(nm)
|
|
157
349
|
for (const n of names) {
|
|
158
350
|
const dir = join(nm, n)
|
|
159
351
|
let link = ''
|
|
352
|
+
let outside: string | null = null
|
|
160
353
|
try {
|
|
161
354
|
if (lstatSync(dir).isSymbolicLink()) {
|
|
162
355
|
const target = realpathSync(dir)
|
|
163
356
|
link = `->${target}`
|
|
164
|
-
|
|
165
|
-
// edit: its content is in neither this repo's git tree nor its
|
|
166
|
-
// version string, so nothing here can describe it
|
|
167
|
-
if (!inRepo(root, target)) uncacheable.push(`${n} links outside the repo (${target})`)
|
|
357
|
+
if (!inRepo(root, target)) outside = target
|
|
168
358
|
}
|
|
169
359
|
} catch {}
|
|
360
|
+
// a `bun link`ed package is live source under someone else's edit:
|
|
361
|
+
// neither this repo's git tree nor its version string describes it, so
|
|
362
|
+
// its files do, and so does what it has installed itself, since its
|
|
363
|
+
// own imports resolve there. Outside the try on purpose: a linked tree
|
|
364
|
+
// that cannot be read throws, and the key layer then keys nothing.
|
|
365
|
+
//
|
|
366
|
+
// The toolkit running this gate is the one exception: each check is
|
|
367
|
+
// keyed on its own closure of it, and hashing it whole here put a css
|
|
368
|
+
// edit of the hub into every content key of every repo that links it
|
|
369
|
+
if (outside != null && outside !== lib.pkgRoot) {
|
|
370
|
+
hashTree(h, outside, `linked:${n}`)
|
|
371
|
+
h.update(`${installIdentity(join(outside, 'node_modules'))}\n`)
|
|
372
|
+
}
|
|
170
373
|
let version = 'MISSING'
|
|
171
374
|
try { version = (JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')) as { version?: string }).version ?? '?' } catch {}
|
|
172
|
-
h.update(`${n}@${version}${link}\n`)
|
|
375
|
+
h.update(`${n}@${version}${link === '' || outside === lib.pkgRoot ? '' : link}\n`)
|
|
173
376
|
}
|
|
174
377
|
// the count catches a removal that leaves every survivor identical
|
|
175
378
|
h.update(`n=${names.length}\n`)
|
|
176
|
-
return
|
|
379
|
+
return h.digest('hex').slice(0, 16)
|
|
177
380
|
}
|
package/src/plan/repo-source.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { Ledger } from '../ledger/ledger.ts'
|
|
|
12
12
|
import { answersDb } from '../ledger/store.ts'
|
|
13
13
|
import { hasWorld, worldKeyOf, type WorldCache } from '../ledger/world-cache.ts'
|
|
14
14
|
import { FOREVER, FOREVER_MS } from '../core/wire.ts'
|
|
15
|
+
import { planned } from '../core/runs.ts'
|
|
15
16
|
import { ONCE, type LastResult } from './cadence.ts'
|
|
16
17
|
import { openDb } from '../ledger/db.ts'
|
|
17
18
|
import { readLastResults } from '../ledger/run-log.ts'
|
|
@@ -21,57 +22,33 @@ import { movedSince } from '../serve/tracked-watch.ts'
|
|
|
21
22
|
import type { RepoConfig } from '../repo-config.ts'
|
|
22
23
|
import type { PlanCheck, PlanReason, PlanSource } from './planner.ts'
|
|
23
24
|
|
|
24
|
-
/** the rows the plan covers:
|
|
25
|
-
*
|
|
25
|
+
/** the rows the plan covers: every live row declaring `onSave` or
|
|
26
|
+
* `onClock` (core/runs.ts). Nothing else is ever planned */
|
|
26
27
|
export function plannable(row: CheckRow): boolean {
|
|
27
|
-
|
|
28
|
-
// nothing to do in this repo on a clock (check.ts planOff)
|
|
29
|
-
if (row.planOff != null) return false
|
|
30
|
-
if (row.optIn) return false
|
|
31
|
-
return !(row.build && row.when.at === 'health')
|
|
28
|
+
return planned(row.runs)
|
|
32
29
|
}
|
|
33
30
|
|
|
34
|
-
/** a REPO check nothing can key and that declares no cadence runs about
|
|
35
|
-
* once a day, never in step, until it declares what it depends on. No
|
|
36
|
-
* built-in may land here (tests/plan-default-range.test.ts) */
|
|
37
|
-
const DEFAULT_EVERY: [string, string] = ['20h', '28h']
|
|
38
|
-
|
|
39
31
|
/** why the plan cannot tell when this check's answer moves, or null when it
|
|
40
|
-
* can
|
|
32
|
+
* can. With every axis explicit, a planned row always says: kept as the
|
|
33
|
+
* burn-down reader for rows declaring a save trigger on nothing keyed */
|
|
41
34
|
export function planGap(row: CheckRow): string | null {
|
|
35
|
+
if (row.runs.onSave !== 'moved') return null
|
|
42
36
|
const pop = populationOf(row.decl)
|
|
43
|
-
if (pop === 'content' || pop === 'reads' ||
|
|
44
|
-
|
|
45
|
-
const why = row.decl.never != null && row.decl.never !== '' ? `never cached (${row.decl.never})` : (row.decl.uncacheable ?? 'declares neither inputs nor ttl')
|
|
46
|
-
return `${why}, and no every: nothing says when its answer moves`
|
|
37
|
+
if (pop === 'content' || pop === 'reads' || isForever(row)) return null
|
|
38
|
+
return `onSave 'moved' but nothing keys it (${row.decl.uncacheable ?? row.decl.declError ?? 'no key'}), so every save moves it`
|
|
47
39
|
}
|
|
48
40
|
|
|
49
|
-
/** one inventory row as a plan check
|
|
50
|
-
*
|
|
51
|
-
* answer replays until a file moves, so a clock re-asks nothing); a repo
|
|
52
|
-
* check with no cadence runs at its own stable point of the default range,
|
|
53
|
-
* and a built-in with none waits for a save and shows on the burn-down */
|
|
41
|
+
/** one inventory row as a plan check, straight from its declared axes:
|
|
42
|
+
* `onClock` is the cadence, `onSave` whether a save can make it due */
|
|
54
43
|
export function planCheckOf(row: CheckRow): PlanCheck {
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
const ttl = row.decl.ttl != null && row.decl.ttl !== '' ? row.decl.ttl : null
|
|
58
|
-
const fallback =
|
|
59
|
-
ttl === FOREVER ? { value: ONCE, why: 'a decided fact: ttl forever' }
|
|
60
|
-
: ttl != null ? { value: ttl, why: 'its ttl' }
|
|
61
|
-
: row.build ? { value: null, why: 'a build step' }
|
|
62
|
-
: keyed ? { value: null, why: 'keyed on its files' }
|
|
63
|
-
: row.from === 'repo' ? { value: DEFAULT_EVERY, why: 'the default range' }
|
|
64
|
-
: { value: null, why: 'a built-in declaring nothing, see shipkit keys --burn-down' }
|
|
65
|
-
// a gate check nothing can key waits for a save only when no cadence
|
|
66
|
-
// says when to run it: a range or an every it declares is the rule
|
|
67
|
-
const clocked = fallback.value != null || row.plan?.every !== undefined || row.plan?.cron != null
|
|
44
|
+
const clock = row.runs.onClock
|
|
45
|
+
const plan = clock === false ? {} : clock === ONCE ? { every: ONCE } : { every: clock.every, whenInactive: clock.inactive }
|
|
68
46
|
return {
|
|
69
47
|
id: row.decl.id,
|
|
70
48
|
title: row.title,
|
|
71
|
-
plan
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
fallback,
|
|
49
|
+
plan,
|
|
50
|
+
onChange: row.runs.onSave !== false,
|
|
51
|
+
fallback: { value: null, why: 'onClock false: only a save runs it' },
|
|
75
52
|
}
|
|
76
53
|
}
|
|
77
54
|
|
|
@@ -182,6 +159,8 @@ export function repoSource(d: RepoSourceDeps): PlanSource {
|
|
|
182
159
|
moved.delete(c.id)
|
|
183
160
|
continue
|
|
184
161
|
}
|
|
162
|
+
// 'burst': every quiet save burst, whatever moved
|
|
163
|
+
if (r.runs.onSave === 'burst') { out.push(c.id); continue }
|
|
185
164
|
const k = ledger.layer.keyFor(c.id)
|
|
186
165
|
if (k.key == null) {
|
|
187
166
|
out.push(c.id)
|
package/src/problems/extract.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { existsSync } from 'fs'
|
|
|
10
10
|
import { isAbsolute, relative, resolve } from 'path'
|
|
11
11
|
import type { Line } from '../core/wire.ts'
|
|
12
12
|
import type { Problem, Severity } from './problem-line.ts'
|
|
13
|
+
import { BUN_RECAP } from '../suite/judge-suites.ts'
|
|
13
14
|
|
|
14
15
|
export type ExtractCtx = {
|
|
15
16
|
/** the repo root every file is made relative to */
|
|
@@ -124,6 +125,8 @@ export function fromBunTest(text: string, ctx: ExtractCtx): Problem[] {
|
|
|
124
125
|
for (const raw of stripAnsi(text).split('\n')) {
|
|
125
126
|
const s = BUN_SUITE.exec(raw)
|
|
126
127
|
if (s != null) { suite = repoFile(ctx, s[1]!, false); error = null; at = null; continue }
|
|
128
|
+
// every line after it repeats a failure already read above
|
|
129
|
+
if (BUN_RECAP.test(raw)) break
|
|
127
130
|
const e = BUN_ERROR.exec(raw)
|
|
128
131
|
if (e != null) { error = e[1]!.trim(); at = null; continue }
|
|
129
132
|
const f = BUN_FRAME.exec(raw)
|
|
@@ -112,6 +112,11 @@ export type ReleaseOpts = {
|
|
|
112
112
|
export async function runReleaseGate(root: string, config: RepoConfig, print: (l: string) => void, o: { fix?: boolean; units?: ReleaseUnit[] } = {}): Promise<number> {
|
|
113
113
|
return await runCi(root, {
|
|
114
114
|
fix: o.fix ?? false,
|
|
115
|
+
// a check's repair never runs on the release path (runs.onRelease):
|
|
116
|
+
// only the mechanical slots (the formatter) may, when bump asks
|
|
117
|
+
fixRules: false,
|
|
118
|
+
// the rows declaring onRelease, whatever the tier
|
|
119
|
+
release: true,
|
|
115
120
|
frozen: true,
|
|
116
121
|
status: false,
|
|
117
122
|
config,
|