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,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 {
|
|
@@ -126,39 +150,56 @@ export class TreeSvc {
|
|
|
126
150
|
if (row.dir) this.toggle(row.key)
|
|
127
151
|
}
|
|
128
152
|
|
|
153
|
+
/** where the arrows start: the cursor, else the section of the page shown */
|
|
154
|
+
private get from(): number {
|
|
155
|
+
const rows = this.rows
|
|
156
|
+
const i = rows.findIndex((r) => r.key === this.cursor)
|
|
157
|
+
if (i !== -1) return i
|
|
158
|
+
const on = this.sections.find((s) => s.on)?.id
|
|
159
|
+
return on == null ? -1 : rows.findIndex((r) => r.section === on)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** an arrow lands on a row and opens its page at once. A branch with
|
|
163
|
+
* nothing to open only takes the cursor: a fold is the chevron's job */
|
|
164
|
+
private land(row: TreeRow): void {
|
|
165
|
+
this.cursor = row.key
|
|
166
|
+
if (row.section != null || row.page != null || row.res != null) this.st.walk(() => this.activate(row))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** a greyed section with no page: the arrows step over it */
|
|
170
|
+
private inert(row: TreeRow): boolean {
|
|
171
|
+
return row.section != null && this.sections.find((s) => s.id === row.section)?.opens === false
|
|
172
|
+
}
|
|
173
|
+
|
|
129
174
|
move(d: number): void {
|
|
130
175
|
const rows = this.rows
|
|
131
176
|
if (rows.length === 0) return
|
|
132
|
-
const i =
|
|
133
|
-
|
|
134
|
-
|
|
177
|
+
const i = this.from
|
|
178
|
+
let next = i === -1 ? (d > 0 ? 0 : rows.length - 1) : i + d
|
|
179
|
+
while (next >= 0 && next < rows.length && this.inert(rows[next]!)) next += d
|
|
180
|
+
if (next < 0 || next >= rows.length || next === i) return
|
|
181
|
+
this.land(rows[next]!)
|
|
135
182
|
}
|
|
136
|
-
/** right descends: a
|
|
183
|
+
/** right descends: a folded row unfolds, anything else steps down */
|
|
137
184
|
right(): void {
|
|
138
185
|
const r = this.cur
|
|
139
|
-
if (r != null && r.dir && !this.
|
|
186
|
+
if (r != null && r.dir && !this.isOpen(r.key)) this.toggle(r.key)
|
|
140
187
|
else this.move(1)
|
|
141
188
|
}
|
|
142
|
-
/** left ascends: an
|
|
189
|
+
/** left ascends: an unfolded row folds, anything else climbs to its parent */
|
|
143
190
|
left(): void {
|
|
144
191
|
const r = this.cur
|
|
145
192
|
if (r == null) return this.move(-1)
|
|
146
|
-
if (r.dir && this.
|
|
193
|
+
if (r.dir && this.isOpen(r.key)) return this.toggle(r.key)
|
|
147
194
|
const rows = this.rows
|
|
148
195
|
const i = rows.findIndex((x) => x.key === r.key)
|
|
149
196
|
for (let n = i - 1; n >= 0; n--) {
|
|
150
|
-
if (rows[n]!.depth < r.depth)
|
|
151
|
-
this.cursor = rows[n]!.key
|
|
152
|
-
return
|
|
153
|
-
}
|
|
197
|
+
if (rows[n]!.depth < r.depth) return this.land(rows[n]!)
|
|
154
198
|
}
|
|
155
199
|
}
|
|
156
|
-
|
|
200
|
+
enter(): void {
|
|
157
201
|
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)
|
|
202
|
+
if (r != null) this.activate(r)
|
|
162
203
|
}
|
|
163
204
|
|
|
164
205
|
/** true = the key was ours, so the page must not also scroll */
|
|
@@ -167,7 +208,7 @@ export class TreeSvc {
|
|
|
167
208
|
else if (k === 'ArrowUp') this.move(-1)
|
|
168
209
|
else if (k === 'ArrowRight') this.right()
|
|
169
210
|
else if (k === 'ArrowLeft') this.left()
|
|
170
|
-
else if (k === 'Enter' || k === ' ') this.
|
|
211
|
+
else if (k === 'Enter' || k === ' ') this.enter()
|
|
171
212
|
else return false
|
|
172
213
|
return true
|
|
173
214
|
}
|
|
@@ -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'
|
|
@@ -203,7 +202,9 @@ export async function resolveHubDeps(root: string, boot: RepoConfig, io: ServeIo
|
|
|
203
202
|
// ONE way a run executes. In process for a hub of its own; as a detached
|
|
204
203
|
// job in the daemon, which follows it: its lines and verdicts reach the
|
|
205
204
|
// same taps, and a reload of the daemon leaves it running
|
|
206
|
-
const exec = async (kind: 'ci' | 'check',
|
|
205
|
+
const exec = async (kind: 'ci' | 'check', sent: Record<string, unknown>, tap: CheckTap, background: boolean): Promise<number> => {
|
|
206
|
+
// the switches a terminal set ride beside the options, into the job's env
|
|
207
|
+
const { env, ...opts } = sent as { env?: Record<string, string> }
|
|
207
208
|
if (jobs == null) {
|
|
208
209
|
const turn = background ? await takeSlot(autoGatePool(), { repo: c.name, check: 'auto gate' }, { pollMs: 250 }) : null
|
|
209
210
|
try {
|
|
@@ -215,7 +216,7 @@ export async function resolveHubDeps(root: string, boot: RepoConfig, io: ServeIo
|
|
|
215
216
|
}
|
|
216
217
|
// a job still running here, started before this image or by a terminal's --detach
|
|
217
218
|
while (liveJobs(root).length > 0) await Bun.sleep(jobs.pollMs ?? 250)
|
|
218
|
-
const j = startJob(root, { cli: jobs.cli, kind, args: [], job: { opts, oneLib: true, turn: background } })
|
|
219
|
+
const j = startJob(root, { cli: jobs.cli, kind, args: [], job: { opts, oneLib: true, turn: background, ...(env == null ? {} : { env }) } })
|
|
219
220
|
tap.job?.(j.id)
|
|
220
221
|
return (await followJob(root, j.id, tap, { pollMs: jobs.pollMs })) ?? 1
|
|
221
222
|
}
|
|
@@ -333,7 +334,6 @@ export async function resolveHubDeps(root: string, boot: RepoConfig, io: ServeIo
|
|
|
333
334
|
root,
|
|
334
335
|
name: c.name,
|
|
335
336
|
uses: c.serve?.uses ?? [],
|
|
336
|
-
processes: new RepoProcesses(root, c.serve?.processes ?? []),
|
|
337
337
|
port,
|
|
338
338
|
graphs,
|
|
339
339
|
studio,
|
|
@@ -443,10 +443,8 @@ export type HubRuntime = {
|
|
|
443
443
|
ping: () => Promise<void>
|
|
444
444
|
/** a run is in flight: a reload now would kill it */
|
|
445
445
|
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
|
|
446
|
+
/** everything this runtime started stops, and it leaves the plan */
|
|
447
|
+
stop: () => void
|
|
450
448
|
}
|
|
451
449
|
|
|
452
450
|
export type LoadHubOpts = {
|
|
@@ -489,7 +487,6 @@ export async function loadHub(root: string, io: ServeIo, o: LoadHubOpts): Promis
|
|
|
489
487
|
return ok
|
|
490
488
|
}
|
|
491
489
|
const handler = hubHandler(deps, { base: o.base, projects: o.projects })
|
|
492
|
-
const procs = deps.processes
|
|
493
490
|
// tick failures are LOUD (the hub's own lines), never swallowed
|
|
494
491
|
const ping = async (): Promise<void> => {
|
|
495
492
|
if (deps.pingTick == null) return
|
|
@@ -519,13 +516,6 @@ export async function loadHub(root: string, io: ServeIo, o: LoadHubOpts): Promis
|
|
|
519
516
|
// the answers a page waits on, computed now, one hub at a time
|
|
520
517
|
// across the process, so a first visit reads them from memory
|
|
521
518
|
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
519
|
if (o.ownTimers) {
|
|
530
520
|
// one tick now (the state file guarantees a restart never re-fires), then every minute
|
|
531
521
|
void ping()
|
|
@@ -541,9 +531,8 @@ export async function loadHub(root: string, io: ServeIo, o: LoadHubOpts): Promis
|
|
|
541
531
|
start,
|
|
542
532
|
ping,
|
|
543
533
|
busy: () => deps.busy?.() === true,
|
|
544
|
-
stop: (
|
|
534
|
+
stop: () => {
|
|
545
535
|
for (const l of live.splice(0)) l.stop()
|
|
546
|
-
if (s?.procs === true) procs?.stopAll()
|
|
547
536
|
handler.stop()
|
|
548
537
|
},
|
|
549
538
|
}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// and the uncommitted count ask git only when a stat says a commit, a tag or
|
|
5
5
|
// the index moved, a few repos per poll, and a repo's run record is opened
|
|
6
6
|
// only when it moved, so a steady poll spawns ps and lsof and nothing else. The icon route
|
|
7
|
-
// serves only the icon files a discovered repo carries,
|
|
7
|
+
// serves only the icon files a discovered repo carries, the app-icon route
|
|
8
|
+
// draws the app icon (core/app-icon.ts) for the admin page's preview, and POST
|
|
8
9
|
// /api/projects/open opens a discovered repo in VS Code, POST
|
|
9
10
|
// /api/projects/fork in Fork: a request names a repo, never a path. The rail ORDER is one text file for the whole machine
|
|
10
11
|
// (ORDER_FILE), so every hub shows the same order and a hand edit is seen
|
|
@@ -21,11 +22,14 @@ import {
|
|
|
21
22
|
import { readLastStatusReadonly } from '../ledger/run-log.ts'
|
|
22
23
|
import { identityOfText, identityProblem, type Identity } from '../core/identity-color.ts'
|
|
23
24
|
import { writeIdentity } from '../core/identity.ts'
|
|
25
|
+
import { appIconOf, appIconPngs, identityOfRoot } from '../core/app-icon.ts'
|
|
24
26
|
import { writeArchived } from '../core/archived.ts'
|
|
25
27
|
|
|
26
28
|
export type { Gate }
|
|
27
29
|
|
|
28
30
|
export const POLL_MS = 5_000
|
|
31
|
+
/** the app icon preview: sharp on a 2x screen at the admin page's 128px */
|
|
32
|
+
const PREVIEW_PX = 256
|
|
29
33
|
/** how long a release answer holds for a checkout whose `.git` is not a
|
|
30
34
|
* folder, since no stat can say when it moved */
|
|
31
35
|
export const RELEASE_TTL_MS = 60_000
|
|
@@ -377,6 +381,23 @@ export function projectGutter(d: GutterDeps) {
|
|
|
377
381
|
headers: { 'content-type': ICON_TYPES[r.icon.split('.').pop()!] ?? 'application/octet-stream', 'cache-control': 'max-age=86400' },
|
|
378
382
|
})
|
|
379
383
|
}
|
|
384
|
+
if (path.startsWith('/api/projects/app-icon/')) {
|
|
385
|
+
if (repos.length === 0) await answer()
|
|
386
|
+
const r = findAny(decodeURIComponent(path.slice('/api/projects/app-icon/'.length)))
|
|
387
|
+
if (r == null) return new Response('not a house repo', { status: 404 })
|
|
388
|
+
const icon = appIconOf(r.path)
|
|
389
|
+
if (icon == null) return new Response('no png mark: the app wears the default icon', { status: 404 })
|
|
390
|
+
// the colours a picker is on right now, before they are saved; else the config's
|
|
391
|
+
const q = new URL(req.url).searchParams
|
|
392
|
+
const asked = { colorLight: q.get('light') ?? '', colorDark: q.get('dark') ?? '' }
|
|
393
|
+
const id = identityProblem(asked) == null ? asked : identityOfRoot(r.path)
|
|
394
|
+
try {
|
|
395
|
+
const [png] = await appIconPngs(await readFile(icon), id, [PREVIEW_PX])
|
|
396
|
+
return new Response(new Uint8Array(png!), { headers: { 'content-type': 'image/png', 'cache-control': 'max-age=60' } })
|
|
397
|
+
} catch (e) {
|
|
398
|
+
return new Response(`could not draw the icon from ${icon}: ${(e as Error).message}`, { status: 422 })
|
|
399
|
+
}
|
|
400
|
+
}
|
|
380
401
|
if (path === '/api/projects/identity' && req.method === 'POST') {
|
|
381
402
|
if (repos.length === 0) await answer()
|
|
382
403
|
const b = (await req.json().catch(() => ({}))) as { name?: unknown; colorLight?: unknown; colorDark?: unknown }
|
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) {
|
|
@@ -294,6 +294,10 @@ export function aloneRunsNeeded(suspects: Timed[], parallelCpu: Record<string, P
|
|
|
294
294
|
return { rerun, cleared }
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
/** the recap bun prints after the last file, "3 tests failed:", repeats
|
|
298
|
+
* every (fail) line under no header of its own: it closes the last file */
|
|
299
|
+
export const BUN_RECAP = /^\s*\d+ tests? failed:\s*$/
|
|
300
|
+
|
|
297
301
|
/** the suites with a failing test, read off bun's report: a file that failed
|
|
298
302
|
* prints its path as a header line, then its `(fail)` lines under it. A
|
|
299
303
|
* passing file prints nothing at all in a piped run, which is why this reads
|
|
@@ -304,6 +308,7 @@ export function failedSuites(output: string): string[] {
|
|
|
304
308
|
for (const line of output.split('\n')) {
|
|
305
309
|
const head = line.match(/^(\S+\.(?:test|spec)\.[cm]?[jt]sx?):$/)
|
|
306
310
|
if (head != null) { cur = normSuitePath(head[1]!); continue }
|
|
311
|
+
if (BUN_RECAP.test(line)) { cur = null; continue }
|
|
307
312
|
// a file that cannot load prints no `(fail)` line, only this banner
|
|
308
313
|
if (cur != null && (/^\(fail\) /.test(line) || line.startsWith('# Unhandled error between tests'))) out.add(cur)
|
|
309
314
|
}
|
|
@@ -322,6 +327,7 @@ export function failLines(output: string): Map<string, string> {
|
|
|
322
327
|
const line = raw.trimEnd()
|
|
323
328
|
const head = line.match(/^(\S+\.(?:test|spec)\.[cm]?[jt]sx?):$/)
|
|
324
329
|
if (head != null) { cur = normSuitePath(head[1]!); err = null; continue }
|
|
330
|
+
if (BUN_RECAP.test(line)) { cur = null; continue }
|
|
325
331
|
if (cur == null || out.has(cur)) continue
|
|
326
332
|
if (err == null && /^\s*error: /.test(line)) err = line.trim()
|
|
327
333
|
const fail = line.match(/^\(fail\) (.*?)(?: \[[\d.]+m?s\])?$/)
|