rvlib-shipkit 0.38.1 → 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 +367 -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 +209 -15
- 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,258 @@
|
|
|
1
|
+
// THE GATE CHECKS, one resource each. Repo health used to be the text of
|
|
2
|
+
// STATUS.md parsed back into one row per wire line, so seventy checks shared
|
|
3
|
+
// one destination, an id was a whole sentence, and nothing could be timed,
|
|
4
|
+
// opened or addressed on its own.
|
|
5
|
+
//
|
|
6
|
+
// Everything here is READ: `gateInventory` is the one enumeration of what the
|
|
7
|
+
// gate runs (ledger/gate-checks.ts), the timings come from the store and the
|
|
8
|
+
// run record beside it, and the last wire lines come from what the last run
|
|
9
|
+
// wrote. Nothing in this file decides anything about a run.
|
|
10
|
+
//
|
|
11
|
+
// THE DEMOTION, which is why `status` is not read off the lines: a check that
|
|
12
|
+
// cannot block has its FAIL rewritten to a warn inside the check engine, so
|
|
13
|
+
// the wire is honest about the GATE and silent about the check. The run
|
|
14
|
+
// record keeps the real outcome, and `advisory` is what says a red here
|
|
15
|
+
// stops nothing.
|
|
16
|
+
import { existsSync, readFileSync } from 'fs'
|
|
17
|
+
import { homedir } from 'os'
|
|
18
|
+
import { isAbsolute, join, relative, resolve } from 'path'
|
|
19
|
+
import { rawName, routesToShipkit } from '../core/canonical-scripts.ts'
|
|
20
|
+
import { FLOOR_IDS } from '../core/floor.ts'
|
|
21
|
+
import { formatLine, parseDuration } from '../core/wire.ts'
|
|
22
|
+
import { gateInventory, type CheckRow } from '../ledger/gate-checks.ts'
|
|
23
|
+
import { readLastLines, readRuns, runsPath, type LastLine, type RunRow } from '../ledger/run-log.ts'
|
|
24
|
+
import { checkStatsFor, type CheckStat } from '../ledger/check-stats.ts'
|
|
25
|
+
import { NEGATIVE_TTL_CAP_MS } from '../ledger/world-cache.ts'
|
|
26
|
+
import type { RepoConfig } from '../repo-config.ts'
|
|
27
|
+
import type { Collection, Instance, NeedSeverity, ResourceState, CheckFacts, RecentRun } from './collection-types.ts'
|
|
28
|
+
import { checkVerdictWords, plural } from '../core/outcome-words.ts'
|
|
29
|
+
|
|
30
|
+
/** what a check is killed at when it names no `timeoutMs`: a build step runs a
|
|
31
|
+
* whole tool chain, everything else answers one question */
|
|
32
|
+
export const CHECK_TIMEOUT_MS = 120_000
|
|
33
|
+
export const BUILD_TIMEOUT_MS = 600_000
|
|
34
|
+
|
|
35
|
+
export type ChecksOpts = {
|
|
36
|
+
/** injected by tests; production reads the store plus the run record */
|
|
37
|
+
stats?: Map<string, CheckStat>
|
|
38
|
+
lastLines?: Record<string, LastLine>
|
|
39
|
+
runs?: RunRow[]
|
|
40
|
+
scripts?: Record<string, string>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** how many outcomes a check's page draws as its bar row. Enough to see a
|
|
44
|
+
* shape, few enough that one wide widget holds them all */
|
|
45
|
+
const KEEP_RUNS = 16
|
|
46
|
+
|
|
47
|
+
/** the last outcomes of every check, oldest first. The AVERAGE hides the
|
|
48
|
+
* shape: what a page draws is the run after run verdict, reds and replays
|
|
49
|
+
* included, which only the log has */
|
|
50
|
+
export function recentRuns(runs: RunRow[], keep = KEEP_RUNS): Record<string, RecentRun[]> {
|
|
51
|
+
const out: Record<string, RecentRun[]> = {}
|
|
52
|
+
for (const r of [...runs].sort((a, b) => a.at - b.at)) {
|
|
53
|
+
const list = out[r.id] ?? []
|
|
54
|
+
list.push({ at: r.at, ms: r.ms, status: r.status, replayed: r.replayed })
|
|
55
|
+
out[r.id] = list.length > keep ? list.slice(-keep) : list
|
|
56
|
+
}
|
|
57
|
+
return out
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function checksCollection(root: string, c: RepoConfig, o: ChecksOpts = {}): Collection {
|
|
61
|
+
return {
|
|
62
|
+
id: 'checks',
|
|
63
|
+
title: 'gate checks',
|
|
64
|
+
media: 'health',
|
|
65
|
+
list: async () => {
|
|
66
|
+
const rows = await gateInventory(root, c)
|
|
67
|
+
const stats = o.stats ?? checkStatsFor(root, rows.map((r) => ({ id: r.decl.id })))
|
|
68
|
+
const lines = o.lastLines ?? readLastLines(runsPath(root))
|
|
69
|
+
const recent = recentRuns(o.runs ?? readRuns(runsPath(root)))
|
|
70
|
+
const scripts = o.scripts ?? readScripts(root)
|
|
71
|
+
return rows.map((row) => {
|
|
72
|
+
const key = row.decl.id
|
|
73
|
+
return instanceOf(root, c, row, stats.get(key), lines[key], recent[key], scriptOf(scripts, row))
|
|
74
|
+
})
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** the package.json scripts, or none when the file is absent or unreadable:
|
|
80
|
+
* the page then shows the argv alone, which is still the truth */
|
|
81
|
+
function readScripts(root: string): Record<string, string> {
|
|
82
|
+
try { return (JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as { scripts?: Record<string, string> }).scripts ?? {} }
|
|
83
|
+
catch { return {} }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** the script behind a build step: the one `<pm> run <name>` names, else for a
|
|
87
|
+
* floor slot the body the floor resolved (`lint:raw`, then `lint` when it
|
|
88
|
+
* does not route back to shipkit), which is what the test slot runs through
|
|
89
|
+
* the suite runner */
|
|
90
|
+
export function scriptOf(scripts: Record<string, string>, row: CheckRow): { name: string; body: string } | null {
|
|
91
|
+
if (!row.build) return null
|
|
92
|
+
const argv = row.decl.argv ?? []
|
|
93
|
+
const floor = (FLOOR_IDS as readonly string[]).includes(row.decl.id)
|
|
94
|
+
const names = [argv[1] === 'run' ? argv[2] : undefined, ...(floor ? [rawName(row.decl.id), row.decl.id] : [])]
|
|
95
|
+
for (const name of names) {
|
|
96
|
+
const body = name == null ? undefined : scripts[name]
|
|
97
|
+
if (name != null && body != null && !routesToShipkit(body)) return { name, body }
|
|
98
|
+
}
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function instanceOf(root: string, c: RepoConfig, row: CheckRow, stat: CheckStat | undefined, last: LastLine | undefined, recent: RecentRun[] | undefined, script: { name: string; body: string } | null = null): Instance {
|
|
103
|
+
const id = row.decl.id
|
|
104
|
+
const wireLines = (last?.lines ?? []).map((l) => formatLine(l, false))
|
|
105
|
+
const status = stat?.last?.status ?? null
|
|
106
|
+
const replayed = stat?.last?.replayed === true
|
|
107
|
+
// the real outcome against what it declared: a FAIL that cannot block
|
|
108
|
+
// stops nothing, and is the one case the wire lines hide
|
|
109
|
+
const advisory = status === 'fail' && row.blocking === false
|
|
110
|
+
const facts: CheckFacts = {
|
|
111
|
+
checkId: id,
|
|
112
|
+
description: row.description,
|
|
113
|
+
lastLines: wireLines,
|
|
114
|
+
gate: row.gate,
|
|
115
|
+
build: row.build,
|
|
116
|
+
blocking: row.blocking,
|
|
117
|
+
advisory,
|
|
118
|
+
optIn: row.optIn,
|
|
119
|
+
...where(row),
|
|
120
|
+
file: sourceFile(root, row),
|
|
121
|
+
doc: docPage(root, c, row),
|
|
122
|
+
cmd: row.decl.argv ?? null,
|
|
123
|
+
script,
|
|
124
|
+
deps: row.deps,
|
|
125
|
+
from: row.from,
|
|
126
|
+
group: row.group,
|
|
127
|
+
inputs: row.decl.inputs ?? undefined,
|
|
128
|
+
ttl: row.decl.ttl ?? undefined,
|
|
129
|
+
never: row.decl.never ?? undefined,
|
|
130
|
+
reads: row.decl.reads === true ? true : undefined,
|
|
131
|
+
timeoutMs: row.timeoutMs ?? (row.build ? BUILD_TIMEOUT_MS : CHECK_TIMEOUT_MS),
|
|
132
|
+
status,
|
|
133
|
+
replayed,
|
|
134
|
+
lastMs: stat?.last == null || replayed ? null : stat.last.ms,
|
|
135
|
+
lastAt: stat?.last?.at ?? null,
|
|
136
|
+
avgMs: stat?.avgMs ?? null,
|
|
137
|
+
samples: stat?.samples ?? 0,
|
|
138
|
+
slowestMs: stat?.slowestMs ?? null,
|
|
139
|
+
failures: stat?.failures ?? 0,
|
|
140
|
+
measuredAt: stat?.measuredAt ?? null,
|
|
141
|
+
measuredStatus: stat?.measuredStatus ?? null,
|
|
142
|
+
keepsUntil: keepsUntil(row.decl.ttl, stat),
|
|
143
|
+
recent: recent ?? [],
|
|
144
|
+
}
|
|
145
|
+
const wire = last?.lines ?? []
|
|
146
|
+
return {
|
|
147
|
+
id: `check:${id}`,
|
|
148
|
+
text: feedLine(row, facts),
|
|
149
|
+
label: rowLabel(row, facts),
|
|
150
|
+
type: 'check',
|
|
151
|
+
...place(root, c, row),
|
|
152
|
+
state: stateOf(facts),
|
|
153
|
+
severity: severityOf(facts),
|
|
154
|
+
check: facts,
|
|
155
|
+
md: wireLines.length === 0 ? undefined : `\`\`\`\n${wireLines.join('\n')}\n\`\`\``,
|
|
156
|
+
file: facts.file ?? undefined,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** when a world probe's remembered answer runs out: its ttl from the last
|
|
161
|
+
* MEASUREMENT, or the shorter negative window when that measurement was not
|
|
162
|
+
* clean (ledger/world-cache.ts rememberWorld keeps the same two). null for a
|
|
163
|
+
* check that remembers nothing on a clock, or was never measured */
|
|
164
|
+
export function keepsUntil(ttl: string | undefined, stat: Pick<CheckStat, 'measuredAt' | 'measuredStatus'> | undefined): number | null {
|
|
165
|
+
if (ttl == null || ttl === '' || stat?.measuredAt == null) return null
|
|
166
|
+
let ms = 0
|
|
167
|
+
try { ms = parseDuration(ttl) } catch { return null }
|
|
168
|
+
if (ms <= 0) return null
|
|
169
|
+
return stat.measuredAt + (stat.measuredStatus === 'pass' ? ms : Math.min(ms, NEGATIVE_TTL_CAP_MS))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** where this check runs today, in the one vocabulary. The inventory already
|
|
173
|
+
* read it (core/when.ts owns the `when` key and the two spellings it
|
|
174
|
+
* replaces), so this only spreads it onto the wire shape */
|
|
175
|
+
function where(row: CheckRow): Pick<CheckFacts, 'when' | 'whenReason' | 'whenSource'> {
|
|
176
|
+
return { when: row.when.at, whenReason: row.when.reason, whenSource: row.when.source }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** a check whose OWNER is the argv it runs: a floor slot and a `tasks: []`
|
|
180
|
+
* entry have a command where a path would be, so neither has a file to open
|
|
181
|
+
* nor a page to link. A `*.check.ts` that declares itself a build step still
|
|
182
|
+
* has both. */
|
|
183
|
+
const ownerIsCommand = (row: CheckRow): boolean => row.owner != null && row.owner === row.decl.argv?.join(' ')
|
|
184
|
+
|
|
185
|
+
/** the file a reader opens to see what this check DOES: its own module, when
|
|
186
|
+
* it has one in this repo. A build step derived from a script is a command,
|
|
187
|
+
* and a built-in ships inside the toolkit, so both answer null. A test check
|
|
188
|
+
* carries its test files as code for its KEY, and none of them is what it is */
|
|
189
|
+
function sourceFile(root: string, row: CheckRow): string | null {
|
|
190
|
+
if (ownerIsCommand(row)) return null
|
|
191
|
+
const code = row.decl.code?.[0]
|
|
192
|
+
return code == null ? null : repoRelative(root, code) ?? code
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** the PAGE that owns the invariant, when the owner names one. A check whose
|
|
196
|
+
* owner is its own file in this repo has no page: `file` already points at
|
|
197
|
+
* it, and a doc link to the same path is one link saying nothing twice. */
|
|
198
|
+
function docPage(root: string, c: RepoConfig, row: CheckRow): string | null {
|
|
199
|
+
if (row.owner == null || ownerIsCommand(row)) return null
|
|
200
|
+
return repoRelative(root, row.owner) != null ? null : `${c.docsBase ?? ''}${row.owner}`
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** the TREE label: the name, and nothing else. The verdict, the duration and
|
|
204
|
+
* where it runs are slots of their own on the row, aligned down the column,
|
|
205
|
+
* and the words behind them are the row's tooltip. */
|
|
206
|
+
function rowLabel(row: CheckRow, f: CheckFacts): string {
|
|
207
|
+
return row.title === f.checkId ? f.checkId : `${f.checkId} ${row.title}`
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** the FEED line: a terminal has no slots, so `shipkit needs` and the ping
|
|
211
|
+
* still need one sentence carrying what it last did and what it costs. */
|
|
212
|
+
function feedLine(row: CheckRow, f: CheckFacts): string {
|
|
213
|
+
const timing = f.lastMs != null ? `, took ${fmtMs(f.lastMs)}` : ''
|
|
214
|
+
const avg = f.avgMs != null && f.samples > 1 ? `, average ${fmtMs(f.avgMs)} over ${plural(f.samples, 'run')}` : ''
|
|
215
|
+
return `${rowLabel(row, f)} ${checkVerdictWords(f.status, f)}${timing}${avg}`
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export const fmtMs = (ms: number): string => (ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${Math.round(ms)}ms`)
|
|
219
|
+
|
|
220
|
+
/** green is MEASURED passing and nothing else may claim it, so a replay is
|
|
221
|
+
* quiet: it is the last verdict, not this tree's evidence. */
|
|
222
|
+
function stateOf(f: CheckFacts): ResourceState {
|
|
223
|
+
if (f.status === 'fail') return f.advisory ? 'warn' : 'red'
|
|
224
|
+
if (f.status === 'warn') return 'warn'
|
|
225
|
+
if (f.status === 'skip') return 'quiet'
|
|
226
|
+
if (f.status === 'pass') return f.replayed ? 'quiet' : 'ok'
|
|
227
|
+
return 'idle'
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** what reaches `shipkit needs` and the ping. An advisory failure is about
|
|
231
|
+
* the world rather than this change, so it is taste debt, never a phone
|
|
232
|
+
* notification at night. */
|
|
233
|
+
function severityOf(f: CheckFacts): NeedSeverity | undefined {
|
|
234
|
+
if (f.status === 'fail') return f.advisory ? 'yellow' : 'red'
|
|
235
|
+
if (f.status === 'warn') return 'yellow'
|
|
236
|
+
return undefined
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** where the check lives on the tree. A check that IS a file in this repo, or
|
|
240
|
+
* whose owner page is one, sits beside everything else in that folder, which
|
|
241
|
+
* is the whole payoff of pathing resources. A build step is a command, so it
|
|
242
|
+
* has no file and says so under a virtual root. */
|
|
243
|
+
function place(root: string, c: RepoConfig, row: CheckRow): Pick<Instance, 'path' | 'virtual'> {
|
|
244
|
+
const virtual = { path: `health/check/${row.decl.id}`, virtual: true as const }
|
|
245
|
+
if (row.owner == null || ownerIsCommand(row)) return virtual
|
|
246
|
+
const rel = repoRelative(root, row.owner) ?? repoRelative(root, `${c.docsBase ?? ''}${row.owner}`)
|
|
247
|
+
return rel == null ? virtual : { path: rel }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** a rule's owner is a page of the docs wiki, which may live in ANOTHER repo
|
|
251
|
+
* entirely (docsBase is a path, `~` included). Only a file inside this repo
|
|
252
|
+
* can be a node of this repo's tree. */
|
|
253
|
+
function repoRelative(root: string, p: string): string | null {
|
|
254
|
+
const abs = resolve(root, p.startsWith('~/') ? join(homedir(), p.slice(2)) : p)
|
|
255
|
+
if (!existsSync(abs)) return null
|
|
256
|
+
const rel = relative(root, abs).replace(/\\/g, '/')
|
|
257
|
+
return rel === '' || rel.startsWith('..') || isAbsolute(rel) ? null : rel
|
|
258
|
+
}
|
|
@@ -6,14 +6,217 @@
|
|
|
6
6
|
// default, or a paste-from-elsewhere. Doctrine page: build/collections in
|
|
7
7
|
// the docs wiki this lib ships beside.
|
|
8
8
|
|
|
9
|
+
import type { WhenAt, WhenSource } from '../core/when.ts'
|
|
10
|
+
import type { CheckFrom } from '../ledger/gate-checks.ts'
|
|
11
|
+
|
|
9
12
|
export type NeedSeverity = 'red' | 'yellow'
|
|
10
13
|
|
|
11
14
|
export type CurationVerb = 'pick' | 'approve' | 'rate' | 'provide' | 'paste-proof' | 'set' | 'done' | 'archive'
|
|
12
15
|
|
|
16
|
+
/** the id the runs page opens by. The page is a PAGE, reached from the
|
|
17
|
+
* header, never a resource of the tree. Its tab rides after it
|
|
18
|
+
* (`runs:files`), so a reload lands on the same list */
|
|
19
|
+
export const RUNS_ID = 'runs'
|
|
20
|
+
|
|
21
|
+
/** the resource id of one test file, from its repo relative path: the tree
|
|
22
|
+
* row, the runs page and the to fix list all open the same page by it */
|
|
23
|
+
export const testId = (file: string): string => `test:${file}`
|
|
24
|
+
|
|
25
|
+
/** where the tree files the test files no check reaches, in both views */
|
|
26
|
+
export const NO_CHECK = 'in no check'
|
|
27
|
+
|
|
28
|
+
/** the resource KIND, never the collection it came from: one folder holds
|
|
29
|
+
* several kinds, which is what makes the path tree worth having. `health`
|
|
30
|
+
* is what belongs to no check (the hub's own staleness line) */
|
|
31
|
+
export const RESOURCE_TYPES = ['ask', 'secret', 'asset', 'post', 'workflow', 'wiki', 'contract', 'health', 'check', 'test', 'reflection'] as const
|
|
32
|
+
|
|
33
|
+
export type ResourceType = (typeof RESOURCE_TYPES)[number]
|
|
34
|
+
|
|
35
|
+
/** what a gate check is and what it last cost. The wire lines say none of
|
|
36
|
+
* this: a non-blocking failure is demoted to a warn before anything reads
|
|
37
|
+
* them, and a duration is nowhere on them at all. */
|
|
38
|
+
/** one outcome of one check, as its page draws it */
|
|
39
|
+
export type RecentRun = { at: number; ms: number; status: 'pass' | 'warn' | 'fail' | 'skip'; replayed: boolean }
|
|
40
|
+
|
|
41
|
+
export type CheckFacts = {
|
|
42
|
+
/** the id `--only` takes, which is NOT the resource id */
|
|
43
|
+
checkId: string
|
|
44
|
+
/** the check's one line description (the shape: check.ts
|
|
45
|
+
* descriptionProblem). Every inventory row carries one, a generated row
|
|
46
|
+
* included (ledger/gate-checks.ts CheckRow) */
|
|
47
|
+
description: string
|
|
48
|
+
/** the wire lines of its last run, already formatted, one per entry. The
|
|
49
|
+
* page renders each one as INLINE markdown, so a `fix:` pointer's url is
|
|
50
|
+
* clickable and its backtick spans read as code, while the line
|
|
51
|
+
* structure the wire protocol fixes stays exactly as it was printed. */
|
|
52
|
+
lastLines: string[]
|
|
53
|
+
/** does `shipkit ci` run it? false = proactive, `shipkit check` only */
|
|
54
|
+
gate: boolean
|
|
55
|
+
/** a BUILD STEP: the gate runs it, the health pass does not (derived from
|
|
56
|
+
* a floor slot or the repo's `tasks: []`, never declared by a check) */
|
|
57
|
+
build: boolean
|
|
58
|
+
/** null = the check never declared, which the check runner treats as fatal */
|
|
59
|
+
blocking: boolean | null
|
|
60
|
+
/** it FAILED and cannot block: orange here, nothing on the gate */
|
|
61
|
+
advisory: boolean
|
|
62
|
+
/** off the default run unless named with `--with` */
|
|
63
|
+
optIn: boolean
|
|
64
|
+
/** where it runs TODAY, in the one vocabulary the switch writes:
|
|
65
|
+
* always (every gate) · release (`--full` and the release path only) ·
|
|
66
|
+
* health (the health pass only) · off (not at all, with a reason) */
|
|
67
|
+
when: WhenAt
|
|
68
|
+
/** mandatory on off, null otherwise */
|
|
69
|
+
whenReason: string | null
|
|
70
|
+
/** which declaration decided it, so a page can say whether the switch is
|
|
71
|
+
* reading `when` or one of the two spellings it replaces */
|
|
72
|
+
whenSource: WhenSource
|
|
73
|
+
/** the file that IS the check, when it has one in this repo. A build step
|
|
74
|
+
* is a command and a built-in ships inside the toolkit, so both say
|
|
75
|
+
* null */
|
|
76
|
+
file: string | null
|
|
77
|
+
/** the page that owns the answer, when the owner is a docs page */
|
|
78
|
+
doc: string | null
|
|
79
|
+
/** the argv a subprocess check runs */
|
|
80
|
+
cmd: string[] | null
|
|
81
|
+
/** a build step filled by a package.json script: its name and the body
|
|
82
|
+
* it expands to, so a page can say what `bun run lint` really runs */
|
|
83
|
+
script?: { name: string; body: string } | null
|
|
84
|
+
/** what it waits on, when it waits on anything */
|
|
85
|
+
deps: string[] | null
|
|
86
|
+
/** whose check it is: `shipkit` = the toolkit ships it, `repo` = this repo
|
|
87
|
+
* declared it (its package.json script behind a floor slot, a task, a
|
|
88
|
+
* rule, a check file) */
|
|
89
|
+
from: CheckFrom
|
|
90
|
+
group: string | null
|
|
91
|
+
/** the cache population it declared: content, world, or decided never */
|
|
92
|
+
inputs?: string[]
|
|
93
|
+
ttl?: string
|
|
94
|
+
never?: string
|
|
95
|
+
/** keyed on what its body read through the check context */
|
|
96
|
+
reads?: boolean
|
|
97
|
+
/** where it is killed: a check creeping toward its ceiling is visible
|
|
98
|
+
* before it trips */
|
|
99
|
+
timeoutMs: number
|
|
100
|
+
status: 'pass' | 'warn' | 'fail' | 'skip' | null
|
|
101
|
+
replayed: boolean
|
|
102
|
+
/** what the last run cost, ms. null on a replay: it measured nothing */
|
|
103
|
+
lastMs: number | null
|
|
104
|
+
lastAt: number | null
|
|
105
|
+
/** mean over the MEASURED runs, reds included */
|
|
106
|
+
avgMs: number | null
|
|
107
|
+
samples: number
|
|
108
|
+
slowestMs: number | null
|
|
109
|
+
failures: number
|
|
110
|
+
/** when it was last MEASURED rather than replayed, and what that said.
|
|
111
|
+
* A remembered answer expires from its measurement */
|
|
112
|
+
measuredAt?: number | null
|
|
113
|
+
measuredStatus?: 'pass' | 'warn' | 'fail' | 'skip' | null
|
|
114
|
+
/** a world probe only: when its remembered answer runs out, so the next
|
|
115
|
+
* health pass after it measures again. null = nothing kept on a clock */
|
|
116
|
+
keepsUntil?: number | null
|
|
117
|
+
/** the last outcomes, oldest first: the shape an average hides */
|
|
118
|
+
recent?: RecentRun[]
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** the declaration of a slow suite, carried by the test file it names. A
|
|
122
|
+
* suite is not a gate check: it is suite granularity inside one task, so it
|
|
123
|
+
* carries its own facts rather than borrowing CheckFacts it cannot fill */
|
|
124
|
+
export type SuiteFacts = {
|
|
125
|
+
/** the suite path as the declaration writes it, relative to the repo root
|
|
126
|
+
* or to the unit's own dir */
|
|
127
|
+
file: string
|
|
128
|
+
/** what it catches that no fast test can */
|
|
129
|
+
catches: string
|
|
130
|
+
/** why it cannot be made fast */
|
|
131
|
+
whyNotFast: string
|
|
132
|
+
/** the config file that declares it */
|
|
133
|
+
configPath: string
|
|
134
|
+
/** the unit whose `tests.slow` holds it, null = the repo root's */
|
|
135
|
+
unit: string | null
|
|
136
|
+
/** the per suite quick tier budget it broke, ms */
|
|
137
|
+
budgetMs: number
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** one recorded run of one test file, newest first on its page. The three
|
|
141
|
+
* optional fields are absent on a row recorded before the record kept them
|
|
142
|
+
* (ledger/suite-log.ts SuiteRow says what each one means) */
|
|
143
|
+
export type TestRun = {
|
|
144
|
+
at: number
|
|
145
|
+
ms: number
|
|
146
|
+
status: 'pass' | 'fail'
|
|
147
|
+
tier: 'quick' | 'full'
|
|
148
|
+
budgetMs: number
|
|
149
|
+
cpuMs?: number | null
|
|
150
|
+
phase?: 'parallel' | 'sequential' | 'serial' | ''
|
|
151
|
+
phaseMs?: number | null
|
|
152
|
+
failLine?: string
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** where a test file stands with the checks: a check runs it, a bunfig
|
|
156
|
+
* ignore keeps it out on purpose, no check reaches it (what SK018 fails),
|
|
157
|
+
* its check's script is not a plain `bun test` so nothing can say, or a slow
|
|
158
|
+
* declaration names a file that is not on disk */
|
|
159
|
+
export type TestStanding = 'runs' | 'ignored' | 'orphan' | 'opaque' | 'missing'
|
|
160
|
+
|
|
161
|
+
/** one test file, as its row and its page read it */
|
|
162
|
+
export type TestFacts = {
|
|
163
|
+
/** repo relative */
|
|
164
|
+
file: string
|
|
165
|
+
/** relative to its unit's dir, the name its row carries */
|
|
166
|
+
inUnit: string
|
|
167
|
+
/** the unit whose dir holds it: `root` or a unit id */
|
|
168
|
+
unit: string
|
|
169
|
+
/** that unit's dir, '' for the root */
|
|
170
|
+
unitDir: string
|
|
171
|
+
/** the check that runs it (or would, for an ignored or opaque file), null
|
|
172
|
+
* when no check reaches it */
|
|
173
|
+
checkId: string | null
|
|
174
|
+
standing: TestStanding
|
|
175
|
+
/** the ignore pattern, or why no check reaches it, or why nothing can say */
|
|
176
|
+
why: string | null
|
|
177
|
+
/** `quick` = every gate, `full` = declared slow, the release path only */
|
|
178
|
+
tier: 'quick' | 'full'
|
|
179
|
+
/** the per file quick tier budget, ms */
|
|
180
|
+
budgetMs: number
|
|
181
|
+
/** THE to fix verdict, from ledger/run-timeline.ts testFilesToFix, the one
|
|
182
|
+
* function the runs page tab reads too. null = nothing to fix */
|
|
183
|
+
toFix: { red: boolean; slow: boolean } | null
|
|
184
|
+
/** the last recorded runs, newest first */
|
|
185
|
+
runs: TestRun[]
|
|
186
|
+
/** how to run it alone: the command, from `cwd` (repo relative, '' the root) */
|
|
187
|
+
cmd: { cwd: string; line: string }
|
|
188
|
+
/** a declared slow suite: the case its declaration argued */
|
|
189
|
+
suite?: SuiteFacts
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** the colour source on a tree label, wider than severity:
|
|
193
|
+
* red = broken · warn = taste pending or a default in use · ok = MEASURED
|
|
194
|
+
* passing (nothing unmeasured may claim it) · idle = healthy and worth
|
|
195
|
+
* reading, the neutral default · quiet = archived, replayed, out of play */
|
|
196
|
+
export type ResourceState = 'red' | 'warn' | 'ok' | 'idle' | 'quiet'
|
|
197
|
+
|
|
13
198
|
export type Instance = {
|
|
14
199
|
id: string
|
|
15
200
|
/** one-line human text — the feed line */
|
|
16
201
|
text: string
|
|
202
|
+
/** the tree label, when the feed line is not one. A row carries a NAME
|
|
203
|
+
* and puts its verdict in the slots beside it; the feed keeps the
|
|
204
|
+
* sentence, because a terminal has no slots */
|
|
205
|
+
label?: string
|
|
206
|
+
/** the ONE short fact this kind shows in the tree's note slot: an ask its
|
|
207
|
+
* kind, a secret present or missing, a post its venue, an asset its roll
|
|
208
|
+
* count. Absent = this kind has nothing to say there */
|
|
209
|
+
slot?: string
|
|
210
|
+
type: ResourceType
|
|
211
|
+
/** a slash path. Where a truth file exists this IS its repo-relative
|
|
212
|
+
* path; otherwise a VIRTUAL path under a root named for the type, and
|
|
213
|
+
* `virtual` says so rather than sending a reader hunting for a file */
|
|
214
|
+
path: string
|
|
215
|
+
/** true when `path` names no file on disk */
|
|
216
|
+
virtual?: boolean
|
|
217
|
+
/** absent = derived from severity (red → red, yellow → warn, none →
|
|
218
|
+
* idle). Set it directly to claim `ok` or `quiet` */
|
|
219
|
+
state?: ResourceState
|
|
17
220
|
/** absent = healthy, nothing needed */
|
|
18
221
|
severity?: NeedSeverity
|
|
19
222
|
/** deep link (studio anchor, /status, external editor) */
|
|
@@ -33,6 +236,10 @@ export type Instance = {
|
|
|
33
236
|
/** the truth file's markdown BODY (frontmatter stripped) — clamped card
|
|
34
237
|
* in the grid, full in the item view; capped server-side, never a dump */
|
|
35
238
|
md?: string
|
|
239
|
+
/** gate checks only: what it is, and what it last cost */
|
|
240
|
+
check?: CheckFacts
|
|
241
|
+
/** test files only: where it runs and what it last cost */
|
|
242
|
+
test?: TestFacts
|
|
36
243
|
/** an open ask owns the action (dedup = acted-on once, never hidden):
|
|
37
244
|
* the instance stays red HERE but verbs and pings live with the ask */
|
|
38
245
|
askOwned?: boolean
|
|
@@ -48,6 +255,52 @@ export type Collection = {
|
|
|
48
255
|
|
|
49
256
|
export type NeedItem = Instance & { collection: string }
|
|
50
257
|
|
|
258
|
+
/** the wire shape the hub tree reads: one flat array for the whole repo.
|
|
259
|
+
* `title` is the row label and `state` its colour, both filled HERE so no
|
|
260
|
+
* provider states them twice. `text` stays as the alias every older
|
|
261
|
+
* consumer reads, and dies with the last provider that writes it */
|
|
262
|
+
export type Resource = Instance & { collection: string; title: string; state: ResourceState }
|
|
263
|
+
|
|
264
|
+
/** PROACTIVE kinds: work waiting on a human, which is not the same thing as
|
|
265
|
+
* something being broken. Red is reserved for broken (a blocking check that
|
|
266
|
+
* failed, a secret a page needs and does not have, a collector that threw),
|
|
267
|
+
* so these read as taste pending on the tree. Their `severity` is untouched,
|
|
268
|
+
* because that is the axis `shipkit needs` and the once ever ping read */
|
|
269
|
+
const PROACTIVE: ReadonlySet<ResourceType> = new Set<ResourceType>(['ask'])
|
|
270
|
+
|
|
271
|
+
/** THE mapping from what a provider declared to the colour a row wears. One
|
|
272
|
+
* place, so a kind cannot be red on the tree and yellow in the feed */
|
|
273
|
+
export const stateOf = (i: Instance): ResourceState =>
|
|
274
|
+
i.state ?? (i.severity === 'red' ? (PROACTIVE.has(i.type) ? 'warn' : 'red') : i.severity === 'yellow' ? 'warn' : 'idle')
|
|
275
|
+
|
|
276
|
+
export async function listResources(c: Collection): Promise<Resource[]> {
|
|
277
|
+
return (await c.list()).map((i) => ({ ...i, collection: c.id, title: i.label ?? i.text, state: stateOf(i) }))
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** every resource of every collection, failure-isolated per collection: a
|
|
281
|
+
* broken source reports itself as ONE red row, never blanks the tree */
|
|
282
|
+
export async function projectResources(collections: Collection[]): Promise<Resource[]> {
|
|
283
|
+
const out: Resource[] = []
|
|
284
|
+
for (const c of collections) {
|
|
285
|
+
try {
|
|
286
|
+
out.push(...(await listResources(c)))
|
|
287
|
+
} catch (e) {
|
|
288
|
+
out.push({
|
|
289
|
+
collection: c.id,
|
|
290
|
+
id: `${c.id}:broken`,
|
|
291
|
+
type: 'health',
|
|
292
|
+
path: `health/${c.id}-collector`,
|
|
293
|
+
virtual: true,
|
|
294
|
+
title: `collector broke: ${(e as Error).message}`,
|
|
295
|
+
text: `collector broke: ${(e as Error).message}`,
|
|
296
|
+
severity: 'red',
|
|
297
|
+
state: 'red',
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return out
|
|
302
|
+
}
|
|
303
|
+
|
|
51
304
|
/** the flat needs projection — failure-isolated per collection: a broken
|
|
52
305
|
* source reports itself as ONE red item, never blanks the feed */
|
|
53
306
|
export async function projectNeeds(collections: Collection[]): Promise<NeedItem[]> {
|
|
@@ -56,7 +309,7 @@ export async function projectNeeds(collections: Collection[]): Promise<NeedItem[
|
|
|
56
309
|
try {
|
|
57
310
|
for (const i of await c.list()) if (i.severity != null) out.push({ ...i, collection: c.id })
|
|
58
311
|
} catch (e) {
|
|
59
|
-
out.push({ collection: c.id, id: `${c.id}:broken`, text: `collector broke: ${(e as Error).message}`, severity: 'red' })
|
|
312
|
+
out.push({ collection: c.id, id: `${c.id}:broken`, type: 'health', path: `health/${c.id}-collector`, virtual: true, text: `collector broke: ${(e as Error).message}`, severity: 'red' })
|
|
60
313
|
}
|
|
61
314
|
}
|
|
62
315
|
return out
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// comfy collection — the repo's workflow modules as section rows, so the
|
|
2
|
+
// sidebar, the grid, the search and the cmd+j palette all reach a graph with
|
|
3
|
+
// no second surface. Every row is QUIET on purpose: a module nobody tuned
|
|
4
|
+
// needs nothing from him, and a GPU box that is off is the world's state, not
|
|
5
|
+
// this repo's (the panel header states it instead).
|
|
6
|
+
import { scanComfyWorkflows } from '../comfy/comfy-panel.ts'
|
|
7
|
+
import type { Collection, Instance } from './collection-types.ts'
|
|
8
|
+
|
|
9
|
+
export function comfyCollection(root: string): Collection {
|
|
10
|
+
return {
|
|
11
|
+
id: 'comfy',
|
|
12
|
+
title: 'comfy',
|
|
13
|
+
media: 'data',
|
|
14
|
+
list: async () =>
|
|
15
|
+
scanComfyWorkflows(root).map((w): Instance => ({
|
|
16
|
+
id: `comfy:${w.id}`,
|
|
17
|
+
text: w.id,
|
|
18
|
+
type: 'workflow',
|
|
19
|
+
path: `.shipkit/comfy/workflows/${w.id}.cflow.ts`,
|
|
20
|
+
file: `.shipkit/comfy/workflows/${w.id}.cflow.ts`,
|
|
21
|
+
})),
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// a truth file's markdown body as a hub card: frontmatter stripped,
|
|
2
|
+
// trimmed, capped. A card, not a document dump, and the cut is loud (…) so a
|
|
3
|
+
// clamped card never passes for the whole file.
|
|
4
|
+
import { existsSync, readFileSync } from 'fs'
|
|
5
|
+
|
|
6
|
+
const MD_CAP = 4000
|
|
7
|
+
|
|
8
|
+
export const mdBody = (path: string): string | undefined => {
|
|
9
|
+
if (!existsSync(path)) return undefined
|
|
10
|
+
const body = readFileSync(path, 'utf8').replace(/^---\n[\s\S]*?\n---\n?/, '').trim()
|
|
11
|
+
if (body === '') return undefined
|
|
12
|
+
// code-point slice, a UTF-16 cut can strand half a surrogate pair
|
|
13
|
+
return body.length > MD_CAP ? `${[...body].slice(0, MD_CAP).join('')}…` : body
|
|
14
|
+
}
|