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
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// THE RUN TIMELINE, shaped from the run record: every invocation on record
|
|
2
|
+
// (`shipkit ci`, `shipkit check`, the hub's buttons and its daily pass), each
|
|
3
|
+
// check a lane with its bar at its real start and end, and three lists beside
|
|
4
|
+
// it: the slowest checks, the longest invocations, the test files to fix.
|
|
5
|
+
//
|
|
6
|
+
// Pure: the record is read by the caller, so every rule here is pinned on a
|
|
7
|
+
// seeded record without a disk. Nothing is estimated. A check's start is its
|
|
8
|
+
// finish minus what it measured, which is exactly what the runner timed
|
|
9
|
+
// (core/check-runner.ts finish), and a replay measured nothing, so its bar is a
|
|
10
|
+
// tick at the moment it was answered.
|
|
11
|
+
import type { InvocationRow, InvokedBy, RunRow, RunStatus } from './run-log.ts'
|
|
12
|
+
import type { SuiteRow } from './suite-log.ts'
|
|
13
|
+
|
|
14
|
+
/** how many invocations the timeline draws, newest first */
|
|
15
|
+
export const TIMELINE_INVOCATIONS = 12
|
|
16
|
+
/** how many rows each list keeps */
|
|
17
|
+
export const TIMELINE_LIST = 12
|
|
18
|
+
/** past this share of its ceiling a check is flagged near it: the same share
|
|
19
|
+
* the hub's duration tone turns red at (hub-app/dur.ts) */
|
|
20
|
+
export const NEAR_CEILING_SHARE = 0.5
|
|
21
|
+
|
|
22
|
+
/** one check run. `running` = still going: its end is the moment the view
|
|
23
|
+
* was shaped, and a drawing carries it to now */
|
|
24
|
+
export type TimelineBar = { id: string; start: number; end: number; ms: number; status: RunStatus; replayed: boolean; running?: boolean }
|
|
25
|
+
|
|
26
|
+
export type TimelineInvocation = {
|
|
27
|
+
runId: string
|
|
28
|
+
/** `unknown` for rows written before invocations were recorded */
|
|
29
|
+
kind: 'ci' | 'check' | 'unknown'
|
|
30
|
+
/** who started it. A starter this build does not name stays the word the
|
|
31
|
+
* record holds */
|
|
32
|
+
by: InvokedBy | 'unknown' | (string & {})
|
|
33
|
+
label: string
|
|
34
|
+
started: number
|
|
35
|
+
ended: number
|
|
36
|
+
wallMs: number
|
|
37
|
+
/** one lane per check, in the order they started */
|
|
38
|
+
bars: TimelineBar[]
|
|
39
|
+
reds: number
|
|
40
|
+
replays: number
|
|
41
|
+
/** still going: `ended` is the moment the view was shaped */
|
|
42
|
+
running?: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** an invocation in a list: its numbers without its lanes */
|
|
46
|
+
export type InvocationSummary = Omit<TimelineInvocation, 'bars'> & { checks: number }
|
|
47
|
+
|
|
48
|
+
export type SlowCheck = { id: string; meanMs: number; worstMs: number; samples: number; ceilingMs: number | null; flag: 'over' | 'near' | null }
|
|
49
|
+
|
|
50
|
+
export type SuiteToFix = {
|
|
51
|
+
check: string; unit: string; file: string; at: number; ms: number; budgetMs: number; tier: 'quick' | 'full'; red: boolean; slow: boolean
|
|
52
|
+
/** the first failure bun printed for it, when the record kept one */
|
|
53
|
+
failLine?: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type Timeline = {
|
|
57
|
+
invocations: TimelineInvocation[]
|
|
58
|
+
slowest: SlowCheck[]
|
|
59
|
+
longest: InvocationSummary[]
|
|
60
|
+
testFiles: SuiteToFix[]
|
|
61
|
+
/** whether a suite record exists at all: an empty list means "nothing to
|
|
62
|
+
* fix" only when something was recorded */
|
|
63
|
+
suitesRecorded: boolean
|
|
64
|
+
/** the runner's cap, when the caller knows it: what the concurrency band
|
|
65
|
+
* draws its line at */
|
|
66
|
+
cap?: number
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type TimelineInput = {
|
|
70
|
+
runs: RunRow[]
|
|
71
|
+
invocations: InvocationRow[]
|
|
72
|
+
suites: SuiteRow[]
|
|
73
|
+
/** each check's kill time, ms */
|
|
74
|
+
ceilings: Record<string, number>
|
|
75
|
+
/** the checks the repo declares today. Given, `slowest` lists only these:
|
|
76
|
+
* a deleted check's rows stay in the run history, and no edit can make it
|
|
77
|
+
* faster */
|
|
78
|
+
inventory?: readonly string[]
|
|
79
|
+
/** suites declared slow: the quick tier leaves them out, so the budget is
|
|
80
|
+
* not theirs to break */
|
|
81
|
+
declaredSlow?: string[]
|
|
82
|
+
cap?: number
|
|
83
|
+
keep?: number
|
|
84
|
+
listSize?: number
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const barOf = (r: RunRow): TimelineBar => ({ id: r.id, start: r.at - r.ms, end: r.at, ms: r.ms, status: r.status, replayed: r.replayed })
|
|
88
|
+
|
|
89
|
+
/** every invocation on record, newest first, each with its lanes */
|
|
90
|
+
export function invocationsOf(runs: RunRow[], invocations: InvocationRow[]): TimelineInvocation[] {
|
|
91
|
+
const byRun = new Map<string, RunRow[]>()
|
|
92
|
+
for (const r of runs) {
|
|
93
|
+
const list = byRun.get(r.runId) ?? []
|
|
94
|
+
list.push(r)
|
|
95
|
+
byRun.set(r.runId, list)
|
|
96
|
+
}
|
|
97
|
+
const meta = new Map(invocations.map((i) => [i.runId, i]))
|
|
98
|
+
const out: TimelineInvocation[] = []
|
|
99
|
+
for (const [runId, rows] of byRun) {
|
|
100
|
+
const bars = rows.map(barOf).sort((a, b) => a.start - b.start || a.id.localeCompare(b.id))
|
|
101
|
+
const inv = meta.get(runId)
|
|
102
|
+
const first = Math.min(...bars.map((b) => b.start))
|
|
103
|
+
const last = Math.max(...bars.map((b) => b.end))
|
|
104
|
+
const started = inv != null ? Math.min(inv.started, first) : first
|
|
105
|
+
const ended = inv != null ? Math.max(inv.ended, last) : last
|
|
106
|
+
out.push({
|
|
107
|
+
runId,
|
|
108
|
+
kind: inv?.kind ?? 'unknown',
|
|
109
|
+
by: inv?.by ?? 'unknown',
|
|
110
|
+
label: inv?.label ?? 'a run recorded before invocations were',
|
|
111
|
+
started,
|
|
112
|
+
ended,
|
|
113
|
+
wallMs: ended - started,
|
|
114
|
+
bars,
|
|
115
|
+
reds: bars.filter((b) => b.status === 'fail').length,
|
|
116
|
+
replays: bars.filter((b) => b.replayed).length,
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
return out.sort((a, b) => b.started - a.started)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** the checks that cost the most when they really run: mean and worst of the
|
|
123
|
+
* MEASURED runs, flagged when the worst reached its ceiling or half of it */
|
|
124
|
+
export function slowestChecks(runs: RunRow[], ceilings: Record<string, number>, size = TIMELINE_LIST, inventory?: readonly string[]): SlowCheck[] {
|
|
125
|
+
const live = inventory == null ? null : new Set(inventory)
|
|
126
|
+
const ms = new Map<string, number[]>()
|
|
127
|
+
for (const r of runs) {
|
|
128
|
+
if (r.replayed || r.status === 'skip') continue
|
|
129
|
+
if (live != null && !live.has(r.id)) continue
|
|
130
|
+
const list = ms.get(r.id) ?? []
|
|
131
|
+
list.push(r.ms)
|
|
132
|
+
ms.set(r.id, list)
|
|
133
|
+
}
|
|
134
|
+
const out: SlowCheck[] = []
|
|
135
|
+
for (const [id, list] of ms) {
|
|
136
|
+
const worstMs = Math.max(...list)
|
|
137
|
+
const ceilingMs = ceilings[id] ?? null
|
|
138
|
+
const flag = ceilingMs == null ? null : worstMs >= ceilingMs ? 'over' : worstMs > ceilingMs * NEAR_CEILING_SHARE ? 'near' : null
|
|
139
|
+
out.push({ id, meanMs: Math.round(list.reduce((a, b) => a + b, 0) / list.length), worstMs, samples: list.length, ceilingMs, flag })
|
|
140
|
+
}
|
|
141
|
+
return out.sort((a, b) => b.meanMs - a.meanMs || a.id.localeCompare(b.id)).slice(0, size)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** each suite's LAST recorded run, kept when it was red or broke the quick
|
|
145
|
+
* tier budget. Red first, then the slowest */
|
|
146
|
+
export function testFilesToFix(suites: SuiteRow[], declaredSlow: string[] = []): SuiteToFix[] {
|
|
147
|
+
const slowDecl = new Set(declaredSlow.map((f) => f.replace(/^\.\//, '')))
|
|
148
|
+
const last = new Map<string, SuiteRow>()
|
|
149
|
+
for (const s of suites) {
|
|
150
|
+
const prev = last.get(s.file)
|
|
151
|
+
if (prev == null || s.at >= prev.at) last.set(s.file, s)
|
|
152
|
+
}
|
|
153
|
+
const out: SuiteToFix[] = []
|
|
154
|
+
for (const s of last.values()) {
|
|
155
|
+
const red = s.status === 'fail'
|
|
156
|
+
const slow = s.tier === 'quick' && s.ms > s.budgetMs && !slowDecl.has(s.file)
|
|
157
|
+
if (red || slow) out.push({ check: s.check, unit: s.unit, file: s.file, at: s.at, ms: s.ms, budgetMs: s.budgetMs, tier: s.tier, red, slow, ...(red && s.failLine != null && s.failLine !== '' ? { failLine: s.failLine } : {}) })
|
|
158
|
+
}
|
|
159
|
+
return out.sort((a, b) => Number(b.red) - Number(a.red) || b.ms - a.ms || a.file.localeCompare(b.file))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function shapeTimeline(o: TimelineInput): Timeline {
|
|
163
|
+
const all = invocationsOf(o.runs, o.invocations)
|
|
164
|
+
const size = o.listSize ?? TIMELINE_LIST
|
|
165
|
+
return {
|
|
166
|
+
invocations: all.slice(0, o.keep ?? TIMELINE_INVOCATIONS),
|
|
167
|
+
slowest: slowestChecks(o.runs, o.ceilings, size, o.inventory),
|
|
168
|
+
longest: [...all].sort((a, b) => b.wallMs - a.wallMs || b.started - a.started).slice(0, size).map(({ bars, ...rest }) => ({ ...rest, checks: bars.length })),
|
|
169
|
+
testFiles: testFilesToFix(o.suites, o.declaredSlow),
|
|
170
|
+
suitesRecorded: o.suites.length > 0,
|
|
171
|
+
...(o.cap == null ? {} : { cap: o.cap }),
|
|
172
|
+
}
|
|
173
|
+
}
|
package/src/ledger/store.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// ONE STORE
|
|
2
|
-
//
|
|
1
|
+
// ONE STORE for every ANSWER this toolkit remembers between runs, in one
|
|
2
|
+
// shape, under one root, busted by one flag. What a run COST lives beside it
|
|
3
|
+
// in `ledger/db.ts`, which nothing about a verdict ever reads.
|
|
3
4
|
//
|
|
4
5
|
// There used to be two: `tmp/.shipkit-cache` for the check orchestrator and
|
|
5
6
|
// `tmp/.check-cache` for the page checks, each with its own bust env var, each
|
|
@@ -24,9 +25,16 @@
|
|
|
24
25
|
// keys and simply miss each other (ledger/toolchain-identity.ts owns the
|
|
25
26
|
// measurement). It is also outside the work tree, so nothing here can dirty a
|
|
26
27
|
// diff or reach a commit.
|
|
28
|
+
//
|
|
29
|
+
// The store dir holds keyed entries and NOTHING else: every file under it is
|
|
30
|
+
// one whole json document, so a reader can walk it and parse all of it. The
|
|
31
|
+
// reporting record beside it (the run db) lives one level up, in
|
|
32
|
+
// `shipkitRoot`, because it is not keyed and is not a json document.
|
|
27
33
|
import { mkdirSync, readdirSync, readFileSync, rmSync, statSync } from 'fs'
|
|
28
34
|
import { dirname, join } from 'path'
|
|
35
|
+
import { gitRoot } from '../core/git-root.ts'
|
|
29
36
|
import { atomicWrite } from '../core/locked-write.ts'
|
|
37
|
+
import { LAYOUT } from '../core/layout.ts'
|
|
30
38
|
|
|
31
39
|
/** the storage FORMAT. Bumped when the envelope changes, so an old file can
|
|
32
40
|
* never be handed to a new reader. The KEY carries everything about the
|
|
@@ -44,7 +52,7 @@ export type Stored<T> = {
|
|
|
44
52
|
key: string
|
|
45
53
|
/** when it was written, ms since epoch */
|
|
46
54
|
at: number
|
|
47
|
-
/** how long THIS entry stays valid, when that is shorter than the
|
|
55
|
+
/** how long THIS entry stays valid, when that is shorter than the check's
|
|
48
56
|
* declared TTL. A measured negative writes one, so a red about the world
|
|
49
57
|
* clears soon after a human fixes the world, while a clean answer keeps
|
|
50
58
|
* for the full declared window. Absent means the declared TTL. */
|
|
@@ -56,19 +64,22 @@ export type Stored<T> = {
|
|
|
56
64
|
* declares, so a live entry is never swept out from under a replay. */
|
|
57
65
|
export const KEEP_MS = 30 * 24 * 60 * 60 * 1000
|
|
58
66
|
|
|
59
|
-
/**
|
|
67
|
+
/** the one dir everything this repo remembers hangs off.
|
|
60
68
|
*
|
|
61
69
|
* `--git-common-dir` rather than `--git-dir`: a linked worktree has its own
|
|
62
70
|
* git dir but shares the common one, and slice 3 measured that sharing is
|
|
63
71
|
* SAFE here because every key component is content or a version string.
|
|
64
|
-
* A tree that is not a git repo at all still gets
|
|
72
|
+
* A tree that is not a git repo at all still gets one, under `.shipkit/cache/`,
|
|
65
73
|
* because a scaffold in progress must not crash on it. */
|
|
66
|
-
export function
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
return dir === '' ? join(root, 'tmp/.shipkit-store') : join(dir, 'shipkit/store')
|
|
74
|
+
export function shipkitRoot(root: string): string {
|
|
75
|
+
const dir = gitRoot(root)?.commonDir
|
|
76
|
+
return dir == null ? join(root, LAYOUT.cache) : join(dir, 'shipkit')
|
|
70
77
|
}
|
|
71
78
|
|
|
79
|
+
/** where this repo's remembered ANSWERS live: keyed entries only, one whole
|
|
80
|
+
* json document per file, nothing else at any depth. */
|
|
81
|
+
export const storeRoot = (root: string): string => join(shipkitRoot(root), 'store')
|
|
82
|
+
|
|
72
83
|
const slug = (s: string): string => s.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 60)
|
|
73
84
|
|
|
74
85
|
/** where ONE answer lives. The key is in the file NAME, so a run whose key
|
|
@@ -81,7 +92,7 @@ export function entryPath(store: string, ns: string, name: string, key: string):
|
|
|
81
92
|
}
|
|
82
93
|
|
|
83
94
|
/** a missing or half-written file reads as "no answer", which is the safe
|
|
84
|
-
* side: the
|
|
95
|
+
* side: the check runs. tmp + rename means half-written cannot happen from a
|
|
85
96
|
* shipkit write, so this only ever catches a truncated file from something
|
|
86
97
|
* else, and treats it the same way. */
|
|
87
98
|
export function readStored<T>(path: string, key: string): Stored<T> | null {
|
|
@@ -106,8 +117,8 @@ export function dropStored(path: string): void {
|
|
|
106
117
|
try { rmSync(path, { force: true }) } catch { /* raced with another runner: it is gone either way */ }
|
|
107
118
|
}
|
|
108
119
|
|
|
109
|
-
/** age-based housekeeping. Sweeping by "the
|
|
110
|
-
* the old prune wrong: a run narrowed with --only knows about one
|
|
120
|
+
/** age-based housekeeping. Sweeping by "the checks I know about" is what made
|
|
121
|
+
* the old prune wrong: a run narrowed with --only knows about one check and
|
|
111
122
|
* would delete the other sixty-five. */
|
|
112
123
|
export function sweepStore(store: string, ns: string, now: number, keepMs = KEEP_MS): void {
|
|
113
124
|
const dir = join(store, slug(ns))
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// THE SUITE RECORD — what every test suite cost and whether it passed, per run
|
|
2
|
+
// of the suite runner (src/suite/suite-cli.ts). The runner already timed every
|
|
3
|
+
// suite for its budget and threw the numbers away with its temp dir, so "which
|
|
4
|
+
// tests are slow, which are red" had no answer after the run.
|
|
5
|
+
//
|
|
6
|
+
// Same file and same rules as the run record (ledger/run-log.ts): never read by
|
|
7
|
+
// the gate, one transaction per write, swept by age with the other tables, and
|
|
8
|
+
// a db that cannot be written costs the history one warn line, never a verdict.
|
|
9
|
+
import { treePrefix } from '../core/nesting.ts'
|
|
10
|
+
import { INSERT_SUITE, INSERT_SUITE_RUN, openDb } from './db.ts'
|
|
11
|
+
import { runsPath, type RunStatus } from './run-log.ts'
|
|
12
|
+
|
|
13
|
+
export type SuiteRow = {
|
|
14
|
+
/** epoch ms, when the run that timed it ended */
|
|
15
|
+
at: number
|
|
16
|
+
/** the suite path, repo relative */
|
|
17
|
+
file: string
|
|
18
|
+
ms: number
|
|
19
|
+
status: Extract<RunStatus, 'pass' | 'fail'>
|
|
20
|
+
tier: 'quick' | 'full'
|
|
21
|
+
/** the per suite budget the quick tier held it to */
|
|
22
|
+
budgetMs: number
|
|
23
|
+
/** the check whose suite runner timed it (`test`, `test:rvlib-shipkit`), '' when
|
|
24
|
+
* the runner ran outside a check or before the record named it */
|
|
25
|
+
check: string
|
|
26
|
+
/** the unit whose tests these are (`root`, `rvlib-shipkit`), '' when
|
|
27
|
+
* the runner was not told, or before the record named it */
|
|
28
|
+
unit: string
|
|
29
|
+
/** CPU the file burned on its own, ms: known only where a process ran it
|
|
30
|
+
* alone (an over budget file run again by itself, a phase of one file).
|
|
31
|
+
* null or absent everywhere else, because a shared worker's CPU is not
|
|
32
|
+
* any one file's */
|
|
33
|
+
cpuMs?: number | null
|
|
34
|
+
/** the runner phase that timed it: `parallel` (the quick tier's workers),
|
|
35
|
+
* `sequential` (the first phase, one file at a time), `serial` (the
|
|
36
|
+
* declared serial files, after it). '' or absent before the record named it */
|
|
37
|
+
phase?: SuitePhase | ''
|
|
38
|
+
/** its wall time in that phase, as bun timed it among the others. `ms` is
|
|
39
|
+
* the time the budget judged, which an alone rerun replaces with the
|
|
40
|
+
* file's own CPU or wall; this is what the file cost the shared run */
|
|
41
|
+
phaseMs?: number | null
|
|
42
|
+
/** the first failure bun printed for it, '' or absent when it passed or
|
|
43
|
+
* before the record kept it */
|
|
44
|
+
failLine?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type SuitePhase = 'parallel' | 'sequential' | 'serial'
|
|
48
|
+
|
|
49
|
+
/** one run of the suite runner as a whole: where its wall time went. The
|
|
50
|
+
* check's own run (the run record) is this plus the process that hosts it */
|
|
51
|
+
export type SuiteRunRow = {
|
|
52
|
+
/** the same `at` as the suite rows it timed */
|
|
53
|
+
at: number
|
|
54
|
+
check: string
|
|
55
|
+
unit: string
|
|
56
|
+
tier: 'quick' | 'full'
|
|
57
|
+
/** the runner, first line to the record write */
|
|
58
|
+
wallMs: number
|
|
59
|
+
/** before the first phase: probing bun, reading the bunfig and the pool */
|
|
60
|
+
setupMs: number
|
|
61
|
+
/** the parallel workers, 0 = one file at a time */
|
|
62
|
+
workers: number
|
|
63
|
+
files: number
|
|
64
|
+
parallelMs: number
|
|
65
|
+
parallelCpuMs: number | null
|
|
66
|
+
/** null = no serial phase */
|
|
67
|
+
serialMs: number | null
|
|
68
|
+
serialCpuMs: number | null
|
|
69
|
+
serialFiles: number
|
|
70
|
+
/** the over budget files, run again one by one before they can fail */
|
|
71
|
+
aloneMs: number
|
|
72
|
+
aloneFiles: number
|
|
73
|
+
/** the machine's one minute load and its cores when the runner judged */
|
|
74
|
+
load1: number | null
|
|
75
|
+
cores: number | null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** how a check runner tells the process it spawns which check it is: the
|
|
79
|
+
* suite runner files every suite it times under it */
|
|
80
|
+
export const CHECK_ID_ENV = 'SHIPKIT_CHECK_ID'
|
|
81
|
+
|
|
82
|
+
/** one write for the whole suite run, each file named from the top of the
|
|
83
|
+
* work tree. Returns the warn line a caller prints when the record could not
|
|
84
|
+
* be written, null when it was */
|
|
85
|
+
export function recordSuites(root: string, rows: SuiteRow[], run?: SuiteRunRow): string | null {
|
|
86
|
+
if (rows.length === 0) return null
|
|
87
|
+
// a unit's runner names its suites from the unit's own dir, while
|
|
88
|
+
// the record and the hub read paths from the top of the work tree
|
|
89
|
+
const prefix = treePrefix(root)
|
|
90
|
+
let path = ''
|
|
91
|
+
try { path = runsPath(root) } catch { return 'WARN suite timings not recorded: the run record has no home here' }
|
|
92
|
+
const opened = openDb(path)
|
|
93
|
+
if (opened == null) return `WARN suite timings not recorded, the run record did not open (${path})`
|
|
94
|
+
try {
|
|
95
|
+
const insert = opened.db.query(INSERT_SUITE)
|
|
96
|
+
opened.db.transaction(() => {
|
|
97
|
+
for (const r of rows)
|
|
98
|
+
insert.run(r.at, `${prefix}${r.file}`, Math.round(r.ms), r.status, r.tier, Math.round(r.budgetMs), r.check, r.unit, r.cpuMs == null ? null : Math.round(r.cpuMs), r.phase ?? '', r.phaseMs == null ? null : Math.round(r.phaseMs), r.failLine ?? '')
|
|
99
|
+
if (run != null)
|
|
100
|
+
opened.db.query(INSERT_SUITE_RUN).run(run.at, run.check, run.unit, run.tier, Math.round(run.wallMs), Math.round(run.setupMs), run.workers, run.files, Math.round(run.parallelMs),
|
|
101
|
+
roundOrNull(run.parallelCpuMs), roundOrNull(run.serialMs), roundOrNull(run.serialCpuMs), run.serialFiles, Math.round(run.aloneMs), run.aloneFiles, run.load1, run.cores)
|
|
102
|
+
}).immediate()
|
|
103
|
+
return null
|
|
104
|
+
} catch {
|
|
105
|
+
return `WARN suite timings not recorded, the run record refused the write (${path})`
|
|
106
|
+
} finally {
|
|
107
|
+
try { opened.db.close() } catch { /* already gone */ }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const roundOrNull = (n: number | null): number | null => (n == null ? null : Math.round(n))
|
|
112
|
+
const numOrNull = (n: unknown): number | null => (n == null ? null : Number(n))
|
|
113
|
+
const PHASES: readonly string[] = ['parallel', 'sequential', 'serial'] satisfies SuitePhase[]
|
|
114
|
+
|
|
115
|
+
/** every suite row on record, oldest first. An unreadable db is none */
|
|
116
|
+
export function readSuites(path: string): SuiteRow[] {
|
|
117
|
+
const opened = openDb(path, { create: false })
|
|
118
|
+
if (opened == null) return []
|
|
119
|
+
try {
|
|
120
|
+
const rows = opened.db.query('select at, file, ms, status, tier, budget_ms as budgetMs, check_id as checkId, unit, cpu_ms as cpuMs, phase, phase_ms as phaseMs, fail_line as failLine from suites order by at asc, rowid asc').all() as {
|
|
121
|
+
at: number; file: string; ms: number; status: string; tier: string; budgetMs: number; checkId: string; unit: string; cpuMs: number | null; phase: string; phaseMs: number | null; failLine: string
|
|
122
|
+
}[]
|
|
123
|
+
return rows.map((r): SuiteRow => ({
|
|
124
|
+
at: Number(r.at), file: String(r.file), ms: Number(r.ms), status: r.status === 'fail' ? 'fail' : 'pass',
|
|
125
|
+
tier: r.tier === 'full' ? 'full' : 'quick', budgetMs: Number(r.budgetMs), check: String(r.checkId ?? ''), unit: String(r.unit ?? ''),
|
|
126
|
+
// each one only where the record holds it, so a row written before
|
|
127
|
+
// the record kept them reads exactly as it was written
|
|
128
|
+
...(r.cpuMs == null ? {} : { cpuMs: Number(r.cpuMs) }),
|
|
129
|
+
...(PHASES.includes(r.phase) ? { phase: r.phase as SuitePhase } : {}),
|
|
130
|
+
...(r.phaseMs == null ? {} : { phaseMs: Number(r.phaseMs) }),
|
|
131
|
+
...(r.failLine == null || r.failLine === '' ? {} : { failLine: String(r.failLine) }),
|
|
132
|
+
}))
|
|
133
|
+
} catch {
|
|
134
|
+
return []
|
|
135
|
+
} finally {
|
|
136
|
+
try { opened.db.close() } catch { /* already gone */ }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** every run of the suite runner on record, oldest first. An unreadable db,
|
|
141
|
+
* or one written before the table existed, is none */
|
|
142
|
+
export function readSuiteRuns(path: string): SuiteRunRow[] {
|
|
143
|
+
const opened = openDb(path, { create: false })
|
|
144
|
+
if (opened == null) return []
|
|
145
|
+
try {
|
|
146
|
+
const rows = opened.db.query(
|
|
147
|
+
'select at, check_id as checkId, unit, tier, wall_ms as wallMs, setup_ms as setupMs, workers, files, parallel_ms as parallelMs, parallel_cpu_ms as parallelCpuMs,' +
|
|
148
|
+
' serial_ms as serialMs, serial_cpu_ms as serialCpuMs, serial_files as serialFiles, alone_ms as aloneMs, alone_files as aloneFiles, load1, cores from suite_runs order by at asc, rowid asc',
|
|
149
|
+
).all() as Record<string, unknown>[]
|
|
150
|
+
return rows.map((r): SuiteRunRow => ({
|
|
151
|
+
at: Number(r.at), check: String(r.checkId ?? ''), unit: String(r.unit ?? ''), tier: r.tier === 'full' ? 'full' : 'quick',
|
|
152
|
+
wallMs: Number(r.wallMs), setupMs: Number(r.setupMs), workers: Number(r.workers), files: Number(r.files),
|
|
153
|
+
parallelMs: Number(r.parallelMs), parallelCpuMs: numOrNull(r.parallelCpuMs), serialMs: numOrNull(r.serialMs), serialCpuMs: numOrNull(r.serialCpuMs),
|
|
154
|
+
serialFiles: Number(r.serialFiles ?? 0), aloneMs: Number(r.aloneMs ?? 0), aloneFiles: Number(r.aloneFiles ?? 0), load1: numOrNull(r.load1), cores: numOrNull(r.cores),
|
|
155
|
+
}))
|
|
156
|
+
} catch {
|
|
157
|
+
return []
|
|
158
|
+
} finally {
|
|
159
|
+
try { opened.db.close() } catch { /* already gone */ }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -38,6 +38,12 @@ export type Toolchain = {
|
|
|
38
38
|
uncacheable: string[]
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/** the gate binary driving THIS process, which is what the key hashes when a
|
|
42
|
+
* caller names none. One owner rather than the same URL written at each
|
|
43
|
+
* entry point, and one place for a caller that must NOT be keyed on the
|
|
44
|
+
* source tree it is running from to hand in something stable instead. */
|
|
45
|
+
export const gateBinary = (): string => new URL('../cli.ts', import.meta.url).pathname
|
|
46
|
+
|
|
41
47
|
/** does this path live inside the repo being gated? */
|
|
42
48
|
const inRepo = (repoRoot: string, p: string): boolean => {
|
|
43
49
|
const r = relative(repoRoot, p)
|
|
@@ -82,7 +88,7 @@ function hashCliTree(h: ReturnType<typeof createHash>, cli: string): void {
|
|
|
82
88
|
* Exported because a repo's own install is not the only one a gate can run
|
|
83
89
|
* against. Sub-packages carry their own `node_modules`, gitignored like every
|
|
84
90
|
* other, so no git key can see them and the root toolchain does not describe
|
|
85
|
-
* them either. A
|
|
91
|
+
* them either. A check whose declared inputs COVER such a directory folds this
|
|
86
92
|
* in beside the path, or the toolchain component stops at the repo root while
|
|
87
93
|
* the task it is meant to describe does not.
|
|
88
94
|
*
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// THE WORLD POPULATION's replay policy, in one place: how long an answer
|
|
2
|
+
// about the world keeps, which answers may be kept at all, and what a kept
|
|
3
|
+
// answer is stamped with.
|
|
4
|
+
//
|
|
5
|
+
// The gate runner remembers a check's `ttl` answer through here. Storage is
|
|
6
|
+
// not this module's concern:
|
|
7
|
+
// ledger/store.ts owns the file, and the CONTENT half of the three
|
|
8
|
+
// populations is keyed instead of timed (ledger/ledger.ts).
|
|
9
|
+
//
|
|
10
|
+
// AN ANSWER CACHES. THE ABSENCE OF ONE DOES NOT. That distinction, not
|
|
11
|
+
// pass-versus-fail, is what decides cacheability here, and a future reader is
|
|
12
|
+
// warned off "fixing" the asymmetry:
|
|
13
|
+
//
|
|
14
|
+
// measured negative a probe that RAN and came back bad ("the box is off",
|
|
15
|
+
// "the token answered 403") is a real answer about the
|
|
16
|
+
// world. Replaying it is CORRECT for a WHILE: the world
|
|
17
|
+
// does not fix itself between two gate runs, and
|
|
18
|
+
// re-asking every 30 seconds spends a third-party API
|
|
19
|
+
// call to be told the same thing. It is NOT correct for
|
|
20
|
+
// the full TTL, because a negative names something a
|
|
21
|
+
// HUMAN is expected to go fix and the fix is invisible
|
|
22
|
+
// from here. So a negative keeps for the cap below at
|
|
23
|
+
// most, while a clean pass keeps for the whole declared
|
|
24
|
+
// window, which is what a long TTL is for.
|
|
25
|
+
// unmeasured a probe that could not run at all (registry
|
|
26
|
+
// unreachable, no cred, rate limited, tool missing)
|
|
27
|
+
// produced no answer. Caching it asserts a dead network
|
|
28
|
+
// as fact long after it recovered, so it stores nothing
|
|
29
|
+
// and EVICTS whatever was stored.
|
|
30
|
+
//
|
|
31
|
+
// A measured negative may be kept only for a check about the WORLD. One about
|
|
32
|
+
// THIS REPO is fixed by a human and the next run goes green, so replaying its
|
|
33
|
+
// red would be the cache lying about work already done.
|
|
34
|
+
import { fmtStamp, type Line } from '../core/wire.ts'
|
|
35
|
+
import { dropStored, entryPath, FRESH_ENV, readStored, writeStored } from './store.ts'
|
|
36
|
+
import { CACHED, RAN } from '../core/outcome-words.ts'
|
|
37
|
+
|
|
38
|
+
/** the longest a MEASURED NEGATIVE is remembered, whatever TTL was declared.
|
|
39
|
+
* Long enough to stop a re-ask storm inside one working session, short
|
|
40
|
+
* enough that the world being fixed is noticed the same day. */
|
|
41
|
+
export const NEGATIVE_TTL_CAP_MS = 60 * 60 * 1000
|
|
42
|
+
|
|
43
|
+
export type WorldCache = {
|
|
44
|
+
/** the ONE store root (ledger/store.ts storeRoot) */
|
|
45
|
+
storeDir: string
|
|
46
|
+
/** which namespace inside it (the repo name for the gate), so a sweep or
|
|
47
|
+
* a manual `rm` can be scoped */
|
|
48
|
+
ns: string
|
|
49
|
+
now: () => number
|
|
50
|
+
/** TEST SEAM: the env var that busts replays. Production is always the one
|
|
51
|
+
* house-wide flag, and a test overrides it so busting one suite cannot
|
|
52
|
+
* poison a neighbour's run. */
|
|
53
|
+
freshEnv?: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const pathFor = (c: WorldCache, name: string, key: string): string => entryPath(c.storeDir, c.ns, name, key)
|
|
57
|
+
|
|
58
|
+
/** the stored answer for this exact question, or null when it must be
|
|
59
|
+
* measured. An entry may keep for LESS than the declared TTL: a measured
|
|
60
|
+
* negative wrote its own shorter window, so the run after a fix sees the fix
|
|
61
|
+
* rather than the memory of the breakage. */
|
|
62
|
+
export function replayWorld(c: WorldCache, name: string, key: string, ttlMs: number): Line[] | null {
|
|
63
|
+
if (ttlMs <= 0) return null
|
|
64
|
+
if (process.env[c.freshEnv ?? FRESH_ENV] != null) return null
|
|
65
|
+
const entry = readStored<Line[]>(pathFor(c, name, key), key)
|
|
66
|
+
if (entry == null) return null
|
|
67
|
+
const keeps = entry.keeps ?? ttlMs
|
|
68
|
+
const now = c.now()
|
|
69
|
+
if (now - entry.at >= keeps || now < entry.at) return null
|
|
70
|
+
// the stamp was baked in when the answer was MEASURED (below): a replay
|
|
71
|
+
// prints the stored lines VERBATIM, so the live run and every later replay
|
|
72
|
+
// render the same string. Annotating replays but not live runs diffed on
|
|
73
|
+
// every fresh-vs-cached alternation, so neither side annotates.
|
|
74
|
+
return entry.payload
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type Measured = {
|
|
78
|
+
lines: Line[]
|
|
79
|
+
/** it passed with nothing to say: no FAIL, no WARN */
|
|
80
|
+
clean: boolean
|
|
81
|
+
/** the probe never reached its subject, so there is no answer to keep */
|
|
82
|
+
unmeasured: boolean
|
|
83
|
+
/** a negative may be kept: this check reports the WORLD rather than this
|
|
84
|
+
* repo (`blocking: false`) */
|
|
85
|
+
keepNegative: boolean
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** what a measured answer leaves behind, and the lines to report for it. This
|
|
89
|
+
* check writes its OWN file and only its own, so a racing runner can lose
|
|
90
|
+
* nothing. */
|
|
91
|
+
export function rememberWorld(c: WorldCache, name: string, key: string, ttlMs: number, m: Measured): Line[] {
|
|
92
|
+
if (ttlMs <= 0) return m.lines
|
|
93
|
+
const cacheable = !m.unmeasured && (m.clean || m.keepNegative)
|
|
94
|
+
const keepsFor = m.clean ? ttlMs : Math.min(ttlMs, NEGATIVE_TTL_CAP_MS)
|
|
95
|
+
const path = pathFor(c, name, key)
|
|
96
|
+
if (!cacheable) {
|
|
97
|
+
// an unmeasured run evicts whatever was stored: a later run must
|
|
98
|
+
// re-probe rather than replay an answer recorded before the outage. A
|
|
99
|
+
// racing runner may have written a good answer into that same file a
|
|
100
|
+
// moment ago and lose it here, which costs one re-probe and can never
|
|
101
|
+
// produce a stale green, so it is the safe direction.
|
|
102
|
+
dropStored(path)
|
|
103
|
+
return m.lines
|
|
104
|
+
}
|
|
105
|
+
const now = c.now()
|
|
106
|
+
const lines = m.lines.map((l, i): Line =>
|
|
107
|
+
i === 0 ? { ...l, text: `${l.text} (${m.clean ? `${CACHED} at ${fmtStamp(now)} until` : `${RAN} at ${fmtStamp(now)}, kept until`} ${fmtStamp(now + keepsFor)})` } : l,
|
|
108
|
+
)
|
|
109
|
+
writeStored(path, key, lines, now, keepsFor)
|
|
110
|
+
return lines
|
|
111
|
+
}
|
package/src/mv/mv-md.ts
CHANGED
|
@@ -14,6 +14,10 @@ export type MvOpts = { write?: boolean; commit?: boolean }
|
|
|
14
14
|
|
|
15
15
|
const sh = (cmd: string, cwd: string): string => execSync(cmd, { cwd }).toString()
|
|
16
16
|
|
|
17
|
+
/** a page's own probe, named after the page: a check file, or a legacy
|
|
18
|
+
* check script, which shares the suffix. */
|
|
19
|
+
const SIBLING_SUFFIXES = ['.check.ts'] as const
|
|
20
|
+
|
|
17
21
|
const posix = (p: string): string => p.replace(/\\/g, '/')
|
|
18
22
|
/** a link points at a file. A directory that happens to share a prose word's
|
|
19
23
|
* spelling is not a reference, and existsSync alone accepts it */
|
|
@@ -114,8 +118,10 @@ export function movePage(root: string, oldRel: string, newRel: string, io: MvIo,
|
|
|
114
118
|
...(oldId != null ? [[`${oldId}.md`, `${newId}.md`] as [string, string]] : []),
|
|
115
119
|
// probe file is named after its page; DIR-qualified so another
|
|
116
120
|
// page's same-named check.ts in a different dir never gets rewritten
|
|
117
|
-
|
|
118
|
-
|
|
121
|
+
...SIBLING_SUFFIXES.flatMap((sfx): [string, string][] => [
|
|
122
|
+
[`${oldBase}/${oldName}${sfx}`, `${newBase}/${newName}${sfx}`],
|
|
123
|
+
...(oldId != null ? [[`${oldId}/${oldName}${sfx}`, `${newId}/${newName}${sfx}`] as [string, string]] : []),
|
|
124
|
+
]),
|
|
119
125
|
[`${oldBase}/`, `${newBase}/`],
|
|
120
126
|
...(oldId != null ? [[`${oldId}/`, `${newId}/`] as [string, string]] : []),
|
|
121
127
|
[oldBase, newBase],
|
|
@@ -134,8 +140,10 @@ export function movePage(root: string, oldRel: string, newRel: string, io: MvIo,
|
|
|
134
140
|
if (existsSync(join(root, oldBase))) {
|
|
135
141
|
moves.set(oldBase, newBase)
|
|
136
142
|
for (const f of tracked) if (f.startsWith(`${oldBase}/`)) moves.set(f, `${newBase}${f.slice(oldBase.length)}`)
|
|
137
|
-
const
|
|
138
|
-
|
|
143
|
+
for (const sfx of SIBLING_SUFFIXES) {
|
|
144
|
+
const probe = `${oldBase}/${oldName}${sfx}`
|
|
145
|
+
if (existsSync(join(root, probe))) moves.set(probe, `${newBase}/${newName}${sfx}`)
|
|
146
|
+
}
|
|
139
147
|
}
|
|
140
148
|
|
|
141
149
|
let touched = 0
|
|
@@ -164,8 +172,10 @@ export function movePage(root: string, oldRel: string, newRel: string, io: MvIo,
|
|
|
164
172
|
// the probe is named after its page, so it only moves when the NAME
|
|
165
173
|
// changed. A move that keeps the name and changes the directory takes
|
|
166
174
|
// the probe along inside the dir, and `git mv X X` is a fatal
|
|
167
|
-
const
|
|
168
|
-
|
|
175
|
+
for (const sfx of SIBLING_SUFFIXES) {
|
|
176
|
+
const oldProbe = join(newBase, `${oldName}${sfx}`)
|
|
177
|
+
if (oldName !== newName && existsSync(join(root, oldProbe))) sh(`git mv ${JSON.stringify(oldProbe)} ${JSON.stringify(join(newBase, `${newName}${sfx}`))}`, root)
|
|
178
|
+
}
|
|
169
179
|
}
|
|
170
180
|
|
|
171
181
|
// repo-specific re-stamps (e.g. wiki depth frontmatter) — loud on failure
|