rvlib-shipkit 1.3.0 → 1.5.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 +105 -0
- package/README.md +17 -17
- package/package.json +3 -2
- package/src/asks/asks-queue.ts +4 -2
- package/src/assets/kits/identity-kit.ts +15 -13
- package/src/assets/run-assets.ts +12 -2
- package/src/check.ts +73 -98
- package/src/checks/SK001-package-json-shape.check.ts +26 -4
- package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
- package/src/checks/SK003-claude-md-shape.check.ts +5 -1
- 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 +94 -74
- 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 +20 -6
- package/src/checks/SK015-nested-repo.check.ts +4 -2
- package/src/checks/SK016-legacy-check-shapes.check.ts +23 -16
- 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 +34 -9
- package/src/checks/SK028-readme.check.ts +10 -2
- package/src/checks/SK029-explicit-check-files.check.ts +95 -0
- package/src/cli/cmd_Asks.ts +5 -5
- package/src/cli/cmd_Assets.ts +7 -7
- package/src/cli/cmd_Bump.ts +12 -11
- 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 +10 -6
- package/src/cli/cmd_Fleet.ts +9 -7
- package/src/cli/cmd_GlobalProcesses.ts +116 -4
- package/src/cli/cmd_Home.ts +23 -17
- package/src/cli/cmd_Hooks.ts +6 -5
- package/src/cli/cmd_Init.ts +5 -4
- package/src/cli/cmd_Keys.ts +8 -7
- package/src/cli/cmd_Mv.ts +7 -6
- package/src/cli/cmd_Needs.ts +4 -3
- package/src/cli/cmd_Overview.ts +4 -3
- 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 +4 -3
- 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_Tui.ts +3 -2
- package/src/cli/cmd_When.ts +71 -0
- package/src/cli/cmd_Wiki.ts +4 -4
- package/src/cli.ts +7 -2
- package/src/collections/checks.ts +2 -0
- package/src/collections/collection-types.ts +24 -4
- package/src/collections/providers.ts +6 -2
- package/src/contracts/apply-contracts.ts +9 -8
- package/src/contracts/contract-types.ts +8 -2
- package/src/contracts/contracts-engine.ts +34 -13
- package/src/contracts/manifests/identity.contract.ts +6 -5
- package/src/contracts/manifests/social.contract.ts +4 -2
- package/src/contracts/manifests/steam.contract.ts +7 -3
- package/src/contracts/web-surface.ts +59 -0
- package/src/core/budget.ts +19 -9
- package/src/core/canonical-scripts.ts +1 -1
- package/src/core/check-runner.ts +63 -25
- package/src/core/config-rewrite.ts +37 -24
- package/src/core/discover-checks.ts +31 -7
- package/src/core/layout.ts +2 -0
- package/src/core/legacy-shapes.ts +39 -10
- package/src/core/motherhome.ts +88 -66
- 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/retention.ts +0 -14
- package/src/core/run-check.ts +14 -14
- package/src/core/run-ci.ts +11 -7
- package/src/core/run-fix.ts +30 -23
- package/src/core/run-fixers.ts +42 -5
- package/src/core/runs-matrix.ts +179 -0
- package/src/core/runs.ts +242 -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 +34 -9
- package/src/fleet/scan-fleet.ts +20 -4
- package/src/fleet/update-fleet.ts +6 -6
- package/src/home/activity-shape.ts +4 -1
- package/src/home/activity.ts +22 -15
- package/src/home/gui-app/ShipkitGui.swift +144 -25
- package/src/home/gui-app.ts +93 -30
- package/src/home/home-launchd.ts +30 -6
- package/src/home/home-routes.ts +14 -2
- package/src/home/home-start.ts +33 -5
- package/src/home/hubs.ts +5 -5
- package/src/home/machine-stores.ts +4 -2
- package/src/home/open-app.ts +23 -10
- package/src/home/pm-config.ts +15 -11
- package/src/home/pm-inputs.ts +76 -0
- package/src/home/pm.ts +11 -3
- package/src/home/process-rows.ts +23 -6
- package/src/home/resources.ts +67 -15
- package/src/home/run-daemon.ts +6 -1
- package/src/home/run-pm.ts +30 -5
- package/src/home/serve-web.ts +2 -2
- package/src/home/supervise.ts +24 -1
- package/src/home/widgets.ts +32 -3
- package/src/hooks/commit-message.ts +2 -3
- package/src/hooks/run-hooks.ts +60 -6
- package/src/init-repo.ts +6 -6
- package/src/ledger/gate-checks.ts +115 -61
- package/src/ledger/git-key.ts +5 -1
- package/src/ledger/ledger.ts +5 -5
- package/src/ledger/store.ts +11 -0
- package/src/ledger/toolchain-identity.ts +45 -27
- package/src/ledger/world-cache.ts +17 -15
- package/src/plan/repo-source.ts +18 -39
- package/src/release/run-release.ts +5 -0
- package/src/repo-config.ts +36 -56
- 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 +4 -4
- package/src/serve/hub-app/admin-view.tsx +13 -11
- 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 +16 -36
- package/src/serve/hub-app/col-tables-view.tsx +131 -0
- package/src/serve/hub-app/col-tables.ts +115 -0
- package/src/serve/hub-app/cols-svc.ts +33 -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 +7 -1
- 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 +15 -3
- package/src/serve/hub-app/home-strip.css +69 -19
- package/src/serve/hub-app/home-strip.tsx +192 -118
- package/src/serve/hub-app/home.html +1 -0
- package/src/serve/hub-app/hub-app.tsx +21 -5
- package/src/serve/hub-app/hub-types.ts +11 -5
- package/src/serve/hub-app/hub.css +88 -44
- package/src/serve/hub-app/icons.tsx +15 -0
- package/src/serve/hub-app/index.html +1 -0
- package/src/serve/hub-app/keys.ts +11 -8
- package/src/serve/hub-app/live-svc.ts +3 -2
- package/src/serve/hub-app/machine-st.ts +34 -49
- package/src/serve/hub-app/palette-st.ts +5 -5
- package/src/serve/hub-app/palette-view.tsx +1 -1
- 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 +38 -0
- package/src/serve/hub-app/project-list.tsx +3 -3
- package/src/serve/hub-app/projects-svc.ts +15 -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 +117 -0
- package/src/serve/hub-app/repo-st.ts +47 -10
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/sidebar.tsx +12 -37
- package/src/serve/hub-app/timeline-svc.ts +3 -2
- package/src/serve/hub-app/timeline-view.tsx +14 -11
- package/src/serve/hub-app/tree-build.ts +149 -155
- package/src/serve/hub-app/tree-row.tsx +39 -9
- package/src/serve/hub-app/tree-svc.ts +70 -49
- 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 +3 -16
- package/src/serve/project-gutter.ts +1 -2
- package/src/serve/run-serve.ts +1 -1
- package/src/serve/serve-hub.ts +4 -14
- package/src/serve/warm-cell.ts +49 -16
- package/src/suite/judge-suites.ts +42 -17
- package/src/suite/suite-cli.ts +5 -8
- package/src/wiki/lint-md.ts +2 -0
- package/src/wiki/scan-md.ts +37 -10
- 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,17 +1,16 @@
|
|
|
1
|
-
// The
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// newline separated
|
|
5
|
-
// on reopen.
|
|
1
|
+
// The left pane's tree, as state: what is unfolded and where the keyboard
|
|
2
|
+
// cursor sits. The trie itself is pure (tree-build.ts) and cached here as
|
|
3
|
+
// computeds, its top rows the sections repo-sections.tsx draws. The folds
|
|
4
|
+
// persist to localStorage as newline separated keys, text encodable and hand
|
|
5
|
+
// editable, and come back on reopen. A live filter unfolds what matches
|
|
6
|
+
// without touching them, so clearing it restores them.
|
|
6
7
|
import { makeAutoObservable } from 'mobx'
|
|
7
8
|
import type { RepoSt } from './repo-st.ts'
|
|
8
9
|
import type { Resource } from './hub-types.ts'
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
export type TreeTab = 'path' | 'type'
|
|
10
|
+
import { repoSections, type RepoSection } from './repo-sections.ts'
|
|
11
|
+
import { buildTree, dirKeys, flattenTree, matchesQuery, passesFilter, processResources, whenCounts, type TreeNode, type TreeRow } from './tree-build.ts'
|
|
12
12
|
|
|
13
13
|
const EXPANDED_KEY = 'shipkit.tree.expanded'
|
|
14
|
-
const TAB_KEY = 'shipkit.tree.tab'
|
|
15
14
|
|
|
16
15
|
// every access is guarded: a private window, blocked site data or a stale
|
|
17
16
|
// origin throws on read AND on write, and a tree that cannot remember is
|
|
@@ -32,44 +31,68 @@ const writeStore = (k: string, v: string): void => {
|
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
export class TreeSvc {
|
|
35
|
-
tab: TreeTab = 'path'
|
|
36
34
|
expanded = new Set<string>()
|
|
37
35
|
/** the row key under the keyboard cursor */
|
|
38
36
|
cursor: string | null = null
|
|
39
37
|
|
|
40
38
|
constructor(private st: RepoSt) {
|
|
41
|
-
this.tab = readStore(TAB_KEY) === 'type' ? 'type' : 'path'
|
|
42
39
|
for (const p of readStore(EXPANDED_KEY).split('\n')) if (p !== '') this.expanded.add(p)
|
|
43
40
|
makeAutoObservable<this, 'st'>(this, { st: false })
|
|
44
41
|
}
|
|
45
42
|
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
private get q(): string {
|
|
44
|
+
return this.st.query.trim().toLowerCase()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** what the tree holds, filtered BEFORE the grouping so it prunes to the
|
|
48
|
+
* branches that still hold something. A search names a thing to find,
|
|
49
|
+
* so it reads every row: the severity group applies only while it is empty */
|
|
50
|
+
/** every row the tree can show: the hub's resources, then the repo's
|
|
51
|
+
* global processes, which pm reports and the hub never lists */
|
|
52
|
+
private get all(): Resource[] {
|
|
53
|
+
return [...this.st.cols.resources, ...processResources(this.st.globalProcesses)]
|
|
54
|
+
}
|
|
55
|
+
|
|
48
56
|
get visible(): Resource[] {
|
|
57
|
+
const all = this.all
|
|
58
|
+
if (this.q !== '') return all.filter((r) => matchesQuery(r, this.q))
|
|
49
59
|
const f = this.st.filter
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
return all.filter((r) => passesFilter(r, f))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** rows the severity group keeps out of the tree, said under it so a
|
|
64
|
+
* pruned tree never reads as a broken one */
|
|
65
|
+
get hidden(): number {
|
|
66
|
+
return this.q !== '' ? 0 : this.all.length - this.visible.length
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** the sections the pane draws, with their counts, keys and pages */
|
|
70
|
+
get sections(): RepoSection[] {
|
|
71
|
+
return repoSections(this.st)
|
|
60
72
|
}
|
|
61
73
|
|
|
62
74
|
get nodes(): TreeNode[] {
|
|
63
|
-
|
|
75
|
+
const all = buildTree(this.sections.map((s) => s.id), this.visible)
|
|
76
|
+
// a search hides every section with nothing matching, unless its own
|
|
77
|
+
// name is what was typed
|
|
78
|
+
if (this.q === '') return all
|
|
79
|
+
return all.filter((n) => n.children.length > 0 || n.label.includes(this.q))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** what is unfolded right now: a live search must SHOW its matches, so
|
|
83
|
+
* the folds yield while it runs, and they are back once it is cleared */
|
|
84
|
+
get open(): Set<string> {
|
|
85
|
+
return this.q === '' ? this.expanded : new Set(dirKeys(this.nodes, false))
|
|
86
|
+
}
|
|
87
|
+
isOpen(key: string): boolean {
|
|
88
|
+
return this.open.has(key)
|
|
64
89
|
}
|
|
65
90
|
|
|
66
91
|
get rows(): TreeRow[] {
|
|
67
|
-
|
|
68
|
-
if (this.st.query.trim() !== '') return flattenTree(this.nodes, new Set(dirKeys(this.nodes, false)))
|
|
69
|
-
return flattenTree(this.nodes, this.expanded)
|
|
92
|
+
return flattenTree(this.nodes, this.open)
|
|
70
93
|
}
|
|
71
94
|
|
|
72
|
-
/** the
|
|
95
|
+
/** the line at the top of the checks page: what runs always and what does not */
|
|
73
96
|
get whenLine(): string {
|
|
74
97
|
const c = whenCounts(this.st.cols.resources)
|
|
75
98
|
const total = c.always + c.release + c.health + c.off
|
|
@@ -83,14 +106,8 @@ export class TreeSvc {
|
|
|
83
106
|
|
|
84
107
|
private save(): void {
|
|
85
108
|
writeStore(EXPANDED_KEY, [...this.expanded].join('\n'))
|
|
86
|
-
writeStore(TAB_KEY, this.tab)
|
|
87
109
|
}
|
|
88
110
|
|
|
89
|
-
setTab(t: TreeTab): void {
|
|
90
|
-
this.tab = t
|
|
91
|
-
this.cursor = null
|
|
92
|
-
this.save()
|
|
93
|
-
}
|
|
94
111
|
toggle(key: string): void {
|
|
95
112
|
this.expanded.has(key) ? this.expanded.delete(key) : this.expanded.add(key)
|
|
96
113
|
this.save()
|
|
@@ -111,14 +128,21 @@ export class TreeSvc {
|
|
|
111
128
|
return this.rowAt(this.cursor)
|
|
112
129
|
}
|
|
113
130
|
|
|
131
|
+
/** what a row opens: a section its page, a group with a page that page,
|
|
132
|
+
* a leaf its own page. Only a row with nothing to open folds */
|
|
133
|
+
private activate(row: TreeRow): void {
|
|
134
|
+
const sec = row.section == null ? null : this.sections.find((s) => s.id === row.section)
|
|
135
|
+
if (sec != null) sec.go()
|
|
136
|
+
else if (row.page != null) this.st.go(row.page)
|
|
137
|
+
else if (row.res != null) this.st.openResource(row.res)
|
|
138
|
+
else if (row.dir) this.toggle(row.key)
|
|
139
|
+
}
|
|
140
|
+
|
|
114
141
|
/** a row click is both a cursor move and the open: one click, one result.
|
|
115
|
-
*
|
|
116
|
-
* folds (fold): the one convention for a row that is both */
|
|
142
|
+
* Only the chevron folds a row that opens something (fold) */
|
|
117
143
|
click(row: TreeRow): void {
|
|
118
144
|
this.cursor = row.key
|
|
119
|
-
|
|
120
|
-
else if (row.dir) this.toggle(row.key)
|
|
121
|
-
else if (row.res != null) this.st.openResource(row.res)
|
|
145
|
+
this.activate(row)
|
|
122
146
|
}
|
|
123
147
|
/** the chevron: fold or unfold, never open */
|
|
124
148
|
fold(row: TreeRow): void {
|
|
@@ -133,17 +157,17 @@ export class TreeSvc {
|
|
|
133
157
|
const next = i === -1 ? (d > 0 ? 0 : rows.length - 1) : Math.min(rows.length - 1, Math.max(0, i + d))
|
|
134
158
|
this.cursor = rows[next]!.key
|
|
135
159
|
}
|
|
136
|
-
/** right descends: a
|
|
160
|
+
/** right descends: a folded row unfolds, anything else steps down */
|
|
137
161
|
right(): void {
|
|
138
162
|
const r = this.cur
|
|
139
|
-
if (r != null && r.dir && !this.
|
|
163
|
+
if (r != null && r.dir && !this.isOpen(r.key)) this.toggle(r.key)
|
|
140
164
|
else this.move(1)
|
|
141
165
|
}
|
|
142
|
-
/** left ascends: an
|
|
166
|
+
/** left ascends: an unfolded row folds, anything else climbs to its parent */
|
|
143
167
|
left(): void {
|
|
144
168
|
const r = this.cur
|
|
145
169
|
if (r == null) return this.move(-1)
|
|
146
|
-
if (r.dir && this.
|
|
170
|
+
if (r.dir && this.isOpen(r.key)) return this.toggle(r.key)
|
|
147
171
|
const rows = this.rows
|
|
148
172
|
const i = rows.findIndex((x) => x.key === r.key)
|
|
149
173
|
for (let n = i - 1; n >= 0; n--) {
|
|
@@ -153,12 +177,9 @@ export class TreeSvc {
|
|
|
153
177
|
}
|
|
154
178
|
}
|
|
155
179
|
}
|
|
156
|
-
|
|
180
|
+
enter(): void {
|
|
157
181
|
const r = this.cur
|
|
158
|
-
if (r
|
|
159
|
-
if (r.page != null) this.st.go(r.page)
|
|
160
|
-
else if (r.dir) this.toggle(r.key)
|
|
161
|
-
else if (r.res != null) this.st.openResource(r.res)
|
|
182
|
+
if (r != null) this.activate(r)
|
|
162
183
|
}
|
|
163
184
|
|
|
164
185
|
/** true = the key was ours, so the page must not also scroll */
|
|
@@ -167,7 +188,7 @@ export class TreeSvc {
|
|
|
167
188
|
else if (k === 'ArrowUp') this.move(-1)
|
|
168
189
|
else if (k === 'ArrowRight') this.right()
|
|
169
190
|
else if (k === 'ArrowLeft') this.left()
|
|
170
|
-
else if (k === 'Enter' || k === ' ') this.
|
|
191
|
+
else if (k === 'Enter' || k === ' ') this.enter()
|
|
171
192
|
else return false
|
|
172
193
|
return true
|
|
173
194
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// A repo's when page: what runs when. The legend first (every context, every
|
|
2
|
+
// value, what it means), then one row per check with its value on each axis,
|
|
3
|
+
// read from the facts every check row already carries (core/runs-matrix.ts),
|
|
4
|
+
// so the page reads nothing of its own and never disagrees with `shipkit when`.
|
|
5
|
+
import { observer } from 'mobx-react-lite'
|
|
6
|
+
import { useState } from 'react'
|
|
7
|
+
import { MATRIX_AXES, RUNS_LEGEND, inColumn, type MatrixAxis } from '../../core/runs-matrix.ts'
|
|
8
|
+
import type { RunsCells } from './hub-types.ts'
|
|
9
|
+
import type { RepoSt } from './repo-st.ts'
|
|
10
|
+
|
|
11
|
+
type Row = { id: string; title: string; cells: RunsCells; blocking: boolean | null; build: boolean }
|
|
12
|
+
|
|
13
|
+
const shortOf = (k: MatrixAxis): string => RUNS_LEGEND.find((a) => a.axis === k)!.short
|
|
14
|
+
|
|
15
|
+
const Legend = () => (
|
|
16
|
+
<table className="whlegend">
|
|
17
|
+
<tbody>
|
|
18
|
+
{RUNS_LEGEND.map((a) => (
|
|
19
|
+
<tr key={a.axis}>
|
|
20
|
+
<td className="whaxis">{a.axis}</td>
|
|
21
|
+
<td>
|
|
22
|
+
<div>{a.context}</div>
|
|
23
|
+
{a.values.map((v) => <div key={v.value} className="whval"><code>{v.value}</code> <span className="dim">{v.means}</span></div>)}
|
|
24
|
+
</td>
|
|
25
|
+
</tr>
|
|
26
|
+
))}
|
|
27
|
+
</tbody>
|
|
28
|
+
</table>
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
const Cell = ({ r, k }: { r: Row; k: MatrixAxis }) => {
|
|
32
|
+
const c = r.cells.cells[k]
|
|
33
|
+
return (
|
|
34
|
+
<td className={`whc${c.on ? ' on' : ''}`} title={`${k}: ${c.text} · from ${c.source}${r.cells.derivedWhy != null ? ` (${r.cells.derivedWhy})` : ''}`}>
|
|
35
|
+
{c.text}
|
|
36
|
+
</td>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const WhenPage = observer(({ app }: { app: RepoSt }) => {
|
|
41
|
+
const [only, setOnly] = useState<MatrixAxis | null>(null)
|
|
42
|
+
const all: Row[] = app.cols.resources
|
|
43
|
+
.filter((r) => r.collection === 'checks' && r.check != null)
|
|
44
|
+
.map((r) => ({ id: r.check!.checkId, title: r.text, cells: r.check!.runs, blocking: r.check!.blocking, build: r.check!.build }))
|
|
45
|
+
const live = all.filter((r) => r.cells.off == null)
|
|
46
|
+
const off = all.filter((r) => r.cells.off != null)
|
|
47
|
+
const shown = only == null ? live : live.filter((r) => inColumn(r.cells, only))
|
|
48
|
+
return (
|
|
49
|
+
<div className="colcard checkcard whpage">
|
|
50
|
+
<div className="chead">
|
|
51
|
+
<span className="tid">when</span>
|
|
52
|
+
<h3 className="ctitle">what runs when</h3>
|
|
53
|
+
</div>
|
|
54
|
+
<div className="usays">every check declares every context, with no default. Click a column to see only what that context runs. `shipkit when` prints the same</div>
|
|
55
|
+
<section className="csec"><div className="cstitle">the legend</div><Legend /></section>
|
|
56
|
+
<section className="csec">
|
|
57
|
+
<div className="cstitle">{only == null ? `${live.length} checks` : `${shown.length} of ${live.length} checks run on ${only}`}{only != null ? <> · <a href="#" onClick={(e) => { e.preventDefault(); setOnly(null) }}>show all</a></> : null}</div>
|
|
58
|
+
<table className="whtable">
|
|
59
|
+
<thead>
|
|
60
|
+
<tr>
|
|
61
|
+
<th>check</th>
|
|
62
|
+
{MATRIX_AXES.map((k) => (
|
|
63
|
+
<th key={k} className={only === k ? 'on' : ''} title={`${k}: ${RUNS_LEGEND.find((a) => a.axis === k)!.context}. Click: only what it runs (${live.filter((r) => inColumn(r.cells, k)).length})`} onClick={() => setOnly(only === k ? null : k)}>
|
|
64
|
+
{shortOf(k)} <span className="dim">{live.filter((r) => inColumn(r.cells, k)).length}</span>
|
|
65
|
+
</th>
|
|
66
|
+
))}
|
|
67
|
+
<th>cache</th>
|
|
68
|
+
</tr>
|
|
69
|
+
</thead>
|
|
70
|
+
<tbody>
|
|
71
|
+
{shown.map((r) => (
|
|
72
|
+
<tr key={r.id}>
|
|
73
|
+
<td className="whid"><a href="#" title={r.title} onClick={(e) => { e.preventDefault(); app.timeline.openCheck(r.id) }}>{r.id}</a>{r.build ? <span className="dim"> build</span> : r.blocking === false ? <span className="dim"> warns</span> : null}</td>
|
|
74
|
+
{MATRIX_AXES.map((k) => <Cell key={k} r={r} k={k} />)}
|
|
75
|
+
<td className="whcache dim" title={r.cells.cache}>{r.cells.cache}</td>
|
|
76
|
+
</tr>
|
|
77
|
+
))}
|
|
78
|
+
</tbody>
|
|
79
|
+
</table>
|
|
80
|
+
</section>
|
|
81
|
+
{off.length > 0 ? (
|
|
82
|
+
<section className="csec">
|
|
83
|
+
<div className="cstitle">off in this repo ({off.length}): repo.config.ts `off`, or the check does not apply here</div>
|
|
84
|
+
{off.map((r) => <div key={r.id} className="whoff"><span className="whid">{r.id}</span> <span className="dim">{r.cells.off}</span></div>)}
|
|
85
|
+
</section>
|
|
86
|
+
) : null}
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/* the when page (when-view.tsx): the legend, then the matrix. A cell that
|
|
2
|
+
runs is washed with the accent, a cell the repo moved carries a star, and
|
|
3
|
+
nothing here picks a colour of its own. */
|
|
4
|
+
.whpage{font-size:13px}
|
|
5
|
+
.whlegend{border-collapse:collapse;margin-bottom:var(--s2)}
|
|
6
|
+
.whlegend td{vertical-align:top;padding:var(--s1) var(--s2);border-bottom:1px solid var(--line)}
|
|
7
|
+
.whaxis{font-family:ui-monospace,monospace;color:var(--accent);white-space:nowrap}
|
|
8
|
+
.whval{padding-left:var(--s3)}
|
|
9
|
+
.whtable{border-collapse:collapse;width:100%}
|
|
10
|
+
.whtable th{position:sticky;top:0;background:var(--panel);text-align:left;font-weight:500;padding:var(--s1) var(--s2);cursor:pointer;white-space:nowrap;border-bottom:1px solid var(--line)}
|
|
11
|
+
.whtable th.on{color:var(--accent);border-bottom-color:var(--accent)}
|
|
12
|
+
.whtable td{padding:var(--s0) var(--s2);border-bottom:1px solid var(--line);white-space:nowrap}
|
|
13
|
+
.whc{color:var(--dim);font-family:ui-monospace,monospace}
|
|
14
|
+
.whc.on{color:var(--ink);background:var(--wash2)}
|
|
15
|
+
.whid{font-family:ui-monospace,monospace}
|
|
16
|
+
.whcache{max-width:320px;overflow:hidden;text-overflow:ellipsis}
|
|
17
|
+
.whoff{padding:var(--s0) var(--s2)}
|
package/src/serve/hub-runtime.ts
CHANGED
|
@@ -44,7 +44,6 @@ import { RUN_JOB } from '../home/home-words.ts'
|
|
|
44
44
|
import { OtherRepos } from './other-repos.ts'
|
|
45
45
|
import { HouseRuns } from './house-runs.ts'
|
|
46
46
|
import { recordStamp } from './project-gutter.ts'
|
|
47
|
-
import { RepoProcesses } from './repo-processes.ts'
|
|
48
47
|
import { shapeTimeline } from '../ledger/run-timeline.ts'
|
|
49
48
|
import { readSuiteRuns, readSuites } from '../ledger/suite-log.ts'
|
|
50
49
|
import { shapeTestCheck, shapeTestsRoot } from '../collections/test-time.ts'
|
|
@@ -333,7 +332,6 @@ export async function resolveHubDeps(root: string, boot: RepoConfig, io: ServeIo
|
|
|
333
332
|
root,
|
|
334
333
|
name: c.name,
|
|
335
334
|
uses: c.serve?.uses ?? [],
|
|
336
|
-
processes: new RepoProcesses(root, c.serve?.processes ?? []),
|
|
337
335
|
port,
|
|
338
336
|
graphs,
|
|
339
337
|
studio,
|
|
@@ -443,10 +441,8 @@ export type HubRuntime = {
|
|
|
443
441
|
ping: () => Promise<void>
|
|
444
442
|
/** a run is in flight: a reload now would kill it */
|
|
445
443
|
busy: () => boolean
|
|
446
|
-
/** everything this runtime started stops, and it leaves the plan
|
|
447
|
-
|
|
448
|
-
* never a process restart */
|
|
449
|
-
stop: (o?: { procs?: boolean }) => void
|
|
444
|
+
/** everything this runtime started stops, and it leaves the plan */
|
|
445
|
+
stop: () => void
|
|
450
446
|
}
|
|
451
447
|
|
|
452
448
|
export type LoadHubOpts = {
|
|
@@ -489,7 +485,6 @@ export async function loadHub(root: string, io: ServeIo, o: LoadHubOpts): Promis
|
|
|
489
485
|
return ok
|
|
490
486
|
}
|
|
491
487
|
const handler = hubHandler(deps, { base: o.base, projects: o.projects })
|
|
492
|
-
const procs = deps.processes
|
|
493
488
|
// tick failures are LOUD (the hub's own lines), never swallowed
|
|
494
489
|
const ping = async (): Promise<void> => {
|
|
495
490
|
if (deps.pingTick == null) return
|
|
@@ -519,13 +514,6 @@ export async function loadHub(root: string, io: ServeIo, o: LoadHubOpts): Promis
|
|
|
519
514
|
// the answers a page waits on, computed now, one hub at a time
|
|
520
515
|
// across the process, so a first visit reads them from memory
|
|
521
516
|
warming = warming.then(() => handler.warm())
|
|
522
|
-
// the repo's own processes: the auto ones start, a restart adopts the
|
|
523
|
-
// ones still running from their pid records
|
|
524
|
-
void procs?.startAuto()
|
|
525
|
-
if (procs != null && procs.size > 0) {
|
|
526
|
-
const t = setInterval(() => void procs.tick(), 2_000)
|
|
527
|
-
live.push({ stop: () => clearInterval(t) })
|
|
528
|
-
}
|
|
529
517
|
if (o.ownTimers) {
|
|
530
518
|
// one tick now (the state file guarantees a restart never re-fires), then every minute
|
|
531
519
|
void ping()
|
|
@@ -541,9 +529,8 @@ export async function loadHub(root: string, io: ServeIo, o: LoadHubOpts): Promis
|
|
|
541
529
|
start,
|
|
542
530
|
ping,
|
|
543
531
|
busy: () => deps.busy?.() === true,
|
|
544
|
-
stop: (
|
|
532
|
+
stop: () => {
|
|
545
533
|
for (const l of live.splice(0)) l.stop()
|
|
546
|
-
if (s?.procs === true) procs?.stopAll()
|
|
547
534
|
handler.stop()
|
|
548
535
|
},
|
|
549
536
|
}
|
|
@@ -14,11 +14,10 @@ import { DB_FILE } from '../ledger/db.ts'
|
|
|
14
14
|
import { defaultHubPort } from './hub-port.ts'
|
|
15
15
|
import { LAYOUT } from '../core/layout.ts'
|
|
16
16
|
import { isArchived } from '../core/archived.ts'
|
|
17
|
+
import { CLAUDE_FILES } from '../home/pm-inputs.ts'
|
|
17
18
|
|
|
18
19
|
export type WiredRepo = { name: string; path: string; icon: string | null }
|
|
19
20
|
|
|
20
|
-
/** the files a repo carries its includes in */
|
|
21
|
-
const CLAUDE_FILES = ['CLAUDE.md', 'CLAUDE.local.md']
|
|
22
21
|
|
|
23
22
|
/** the absolute md file of every `@<path>.md` include line a repo's CLAUDE
|
|
24
23
|
* files carry, in order. A sentence that mentions a path is no include */
|
package/src/serve/run-serve.ts
CHANGED
package/src/serve/serve-hub.ts
CHANGED
|
@@ -28,7 +28,6 @@ import type { HubProblems } from './hub-problems.ts'
|
|
|
28
28
|
import hubIndex from './hub-app/index.html'
|
|
29
29
|
import { homePort, homeUp } from '../home/home-paths.ts'
|
|
30
30
|
import { RUN_EXIT } from '../home/home-words.ts'
|
|
31
|
-
import type { RepoProcesses } from './repo-processes.ts'
|
|
32
31
|
import { projectGutter } from './project-gutter-route.ts'
|
|
33
32
|
import { sameOrigin } from './same-origin.ts'
|
|
34
33
|
import { LAYOUT } from '../core/layout.ts'
|
|
@@ -54,8 +53,6 @@ export type HubDeps = {
|
|
|
54
53
|
name: string
|
|
55
54
|
/** the machine resources the repo declares in serve.uses */
|
|
56
55
|
uses?: string[]
|
|
57
|
-
/** the long running processes the repo declares in serve.processes */
|
|
58
|
-
processes?: RepoProcesses
|
|
59
56
|
/** a terminal's `shipkit ci` or `check`, run in this process; resolves to its exit code */
|
|
60
57
|
runCli?: (kind: 'ci' | 'check', opts: Record<string, unknown>, print: (l: string) => void) => Promise<number>
|
|
61
58
|
/** 0 = random (tests); real launches pass repo.config/default */
|
|
@@ -235,13 +232,6 @@ export function hubHandler(d: HubDeps, o: { base?: string; projects?: Gutter } =
|
|
|
235
232
|
})
|
|
236
233
|
return new Response(stream, { headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' } })
|
|
237
234
|
}
|
|
238
|
-
if (path === '/api/processes.json') return Response.json({ processes: d.processes?.views() ?? [] })
|
|
239
|
-
if (path === '/api/processes/act' && req.method === 'POST') {
|
|
240
|
-
const b = (await req.json().catch(() => ({}))) as { id?: string; act?: string }
|
|
241
|
-
if (b.act !== 'start' && b.act !== 'stop' && b.act !== 'restart') return new Response('act must be start, stop or restart', { status: 400 })
|
|
242
|
-
const v = d.processes?.act(b.id ?? '', b.act)
|
|
243
|
-
return v == null ? new Response(`no process '${b.id ?? ''}' in serve.processes`, { status: 404 }) : Response.json(v)
|
|
244
|
-
}
|
|
245
235
|
// the three surfaces built on the config read the config on disk
|
|
246
236
|
if (path === '/api/needs.json' || path === '/api/resources.json' || path === '/api/checks.json' || path === '/api/collections.json') await d.refresh?.()
|
|
247
237
|
if (path === '/api/needs.json') {
|
|
@@ -249,9 +239,9 @@ export function hubHandler(d: HubDeps, o: { base?: string; projects?: Gutter } =
|
|
|
249
239
|
return Response.json({ needs })
|
|
250
240
|
}
|
|
251
241
|
if (path === '/api/resources.json') {
|
|
252
|
-
// ONE flat list for the whole repo: the page groups it
|
|
253
|
-
//
|
|
254
|
-
// answer to drift from this one
|
|
242
|
+
// ONE flat list for the whole repo: the page groups it
|
|
243
|
+
// under its sections, and a second endpoint would be a
|
|
244
|
+
// second answer to drift from this one
|
|
255
245
|
const r = await rows.get({ fresh: fresh(req) })
|
|
256
246
|
return Response.json({
|
|
257
247
|
collections: r.collections,
|
|
@@ -377,7 +367,7 @@ export function hubHandler(d: HubDeps, o: { base?: string; projects?: Gutter } =
|
|
|
377
367
|
if (req.method === 'POST') {
|
|
378
368
|
try {
|
|
379
369
|
const body = (await req.json()) as GateEdit & { expect?: string }
|
|
380
|
-
const b = await writeGateBlock(d.root, {
|
|
370
|
+
const b = await writeGateBlock(d.root, { off: body.off, slow: body.slow, slowRemove: body.slowRemove, words: body.words }, { expect: body.expect })
|
|
381
371
|
await d.refresh?.()
|
|
382
372
|
return Response.json({ ...b, dirty: configDirty(d.root) })
|
|
383
373
|
} catch (e) {
|
package/src/serve/warm-cell.ts
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
// one answer of a hub kept warm in memory: computed when the hub loads,
|
|
2
2
|
// recomputed in the background when something says it moved (a save, a
|
|
3
|
-
// stamp, its age), and served from memory. A
|
|
4
|
-
//
|
|
3
|
+
// stamp, its age) while a page reads it, and served from memory. A cell
|
|
4
|
+
// nobody read for WARM_WATCH_MS computes nothing on a save: its next reader
|
|
5
|
+
// waits on one compute instead, so an unwatched repo costs no cpu per save.
|
|
5
6
|
//
|
|
6
|
-
// the one constraint a reader breaks first: `get()`
|
|
7
|
-
// memory even when it is known stale. Whoever just
|
|
8
|
-
// act, the end of a run) asks for `fresh`, which waits
|
|
9
|
-
// started after its call.
|
|
7
|
+
// the one constraint a reader breaks first: `get()` from a watched cell
|
|
8
|
+
// answers what is in memory even when it is known stale. Whoever just
|
|
9
|
+
// changed the truth (an act, the end of a run) asks for `fresh`, which waits
|
|
10
|
+
// on a compute that started after its call.
|
|
11
|
+
|
|
12
|
+
/** a cell read within this long has a reader: a save recomputes it in the
|
|
13
|
+
* background. Past it, nothing recomputes until a reader asks */
|
|
14
|
+
export const WARM_WATCH_MS = 30_000
|
|
15
|
+
|
|
16
|
+
/** a background recompute waits this many times the last compute's length
|
|
17
|
+
* after it ended: a 1.5s answer at most every 15s, a 50ms one at once */
|
|
18
|
+
export const WARM_SPACING = 10
|
|
10
19
|
|
|
11
20
|
export type WarmCellOpts = {
|
|
12
21
|
/** a cheap fingerprint of what the answer reads (file stats): a new one
|
|
@@ -20,6 +29,10 @@ export type WarmCellOpts = {
|
|
|
20
29
|
|
|
21
30
|
export class WarmCell<T> {
|
|
22
31
|
private value: { v: T; at: number; stamp: string } | null = null
|
|
32
|
+
/** when a reader last asked, null before any */
|
|
33
|
+
private readAt: number | null = null
|
|
34
|
+
/** when the last compute ended and how long it took */
|
|
35
|
+
private last: { end: number; ms: number } | null = null
|
|
23
36
|
private running: Promise<T> | null = null
|
|
24
37
|
/** bumped by invalidate: a compute that started before it is stale */
|
|
25
38
|
private gen = 0
|
|
@@ -37,6 +50,7 @@ export class WarmCell<T> {
|
|
|
37
50
|
const gen = this.gen
|
|
38
51
|
const stamp = this.o.stamp?.() ?? ''
|
|
39
52
|
this.runGen = gen
|
|
53
|
+
const start = this.now()
|
|
40
54
|
const p = (async (): Promise<T> => {
|
|
41
55
|
try {
|
|
42
56
|
// never synchronously: `running` is set before this settles
|
|
@@ -44,6 +58,8 @@ export class WarmCell<T> {
|
|
|
44
58
|
this.value = { v, at: this.now(), stamp }
|
|
45
59
|
return v
|
|
46
60
|
} finally {
|
|
61
|
+
const end = this.now()
|
|
62
|
+
this.last = { end, ms: end - start }
|
|
47
63
|
this.running = null
|
|
48
64
|
// what said it moved while this ran is not in this answer
|
|
49
65
|
if (this.gen !== gen) this.kick()
|
|
@@ -53,11 +69,19 @@ export class WarmCell<T> {
|
|
|
53
69
|
return p
|
|
54
70
|
}
|
|
55
71
|
|
|
56
|
-
/** a compute in the background
|
|
72
|
+
/** a compute in the background, at most once per WARM_SPACING times the
|
|
73
|
+
* last one: a kick too soon is dropped, and the next read past the gap
|
|
74
|
+
* kicks again. On a failure the last answer stays */
|
|
57
75
|
private kick(): void {
|
|
76
|
+
if (this.last != null && this.now() < this.last.end + WARM_SPACING * this.last.ms) return
|
|
58
77
|
void this.run().catch(() => {})
|
|
59
78
|
}
|
|
60
79
|
|
|
80
|
+
/** a reader asked within WARM_WATCH_MS */
|
|
81
|
+
private get watched(): boolean {
|
|
82
|
+
return this.readAt != null && this.now() - this.readAt < WARM_WATCH_MS
|
|
83
|
+
}
|
|
84
|
+
|
|
61
85
|
get stale(): boolean {
|
|
62
86
|
const v = this.value
|
|
63
87
|
if (v == null) return true
|
|
@@ -68,23 +92,32 @@ export class WarmCell<T> {
|
|
|
68
92
|
/** the answer in memory, a stale one kicking a recompute behind it. With
|
|
69
93
|
* nothing in memory yet, or `fresh`, it waits on a compute */
|
|
70
94
|
async get(o: { fresh?: boolean } = {}): Promise<T> {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (this.running != null) await this.running.catch(() => {})
|
|
75
|
-
return this.run()
|
|
76
|
-
}
|
|
95
|
+
const watched = this.watched
|
|
96
|
+
this.readAt = this.now()
|
|
97
|
+
if (o.fresh === true) return this.fresh()
|
|
77
98
|
const v = this.value
|
|
78
99
|
// nothing in memory, a failed compute included: wait on a new one
|
|
79
100
|
if (v == null) return this.run()
|
|
80
|
-
|
|
101
|
+
const moved = this.stale || this.gen !== this.runGen
|
|
102
|
+
// the first reader in a while: what is in memory may be hours old,
|
|
103
|
+
// and no save recomputed it meanwhile
|
|
104
|
+
if (moved && !watched) return this.running != null && this.runGen === this.gen ? this.running : this.fresh()
|
|
105
|
+
if (this.running == null && moved) this.kick()
|
|
81
106
|
return v.v
|
|
82
107
|
}
|
|
83
108
|
|
|
84
|
-
/**
|
|
109
|
+
/** a compute that started after this call: one that started before may
|
|
110
|
+
* miss what the caller just changed, so wait for it, then for our own */
|
|
111
|
+
private async fresh(): Promise<T> {
|
|
112
|
+
if (this.running != null) await this.running.catch(() => {})
|
|
113
|
+
return this.run()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** something this answer reads moved: recompute in the background while
|
|
117
|
+
* a reader watches, else leave it to the next reader */
|
|
85
118
|
invalidate(): void {
|
|
86
119
|
this.gen++
|
|
87
|
-
if (this.running == null) this.kick()
|
|
120
|
+
if (this.running == null && this.watched) this.kick()
|
|
88
121
|
}
|
|
89
122
|
|
|
90
123
|
/** compute now unless an answer is already in memory: the hub's load */
|